#!/bin/sh
# shellcheck disable=1007,2015,2031,2164,2317,3043

# Requires mktemp(1), which is not a standard utility, but is commonly
# available. The implementations provided by GNU coreutils, busybox and toybox
# are known to be compatible.

bailout() {
	printf 'Bail out! %s.\n' "$1"
	cleanup_tmpdir
	exit 1
}

assign_tmpdir() {
	global_tmpdir=$(mktemp -d) \
	&& chdir "${global_tmpdir}" \
	|| bailout "Couldn't create or change to the temp dir"
}

cleanup_tmpdir() {
	if [ "${global_tmpdir}" ]; then
		rm -r -- "${global_tmpdir}"
	fi
}

test_local() {
	set -- eq 0

	callback() {
		test_description="/bin/sh supports local"
		(
			var=1
			f() {
				local var=2
				g
				test "${var}" = 3 || exit
			}
			g() {
				test "${var}" = 2 || exit
				var=3
			}
			f
			test "${var}" = 1
		) 2>/dev/null
	}

	iterate_tests 2 "$@"
}

test_chdir() {
	set -- \
		ge  1          ''  \
		ge  1  grandchild  \
		ge  1         var  \
		eq  0          -L  \
		eq  0          -p  \
		eq  0          -e  \
		eq  0          -@  \
		eq  0           -  \
		eq  0       child

	if ! mkdir -p -- -L -p -e -@ - child child/grandchild; then
		bailout "Couldn't set up all test directories"
	fi

	callback() {
		local CDPATH var

		shift
		test_description="chdir $(quote_args "$@")"
		if [ "$BASH" ]; then
			# shellcheck disable=3044
			shopt -s cdable_vars
		fi
		CDPATH=child
		var=child
		chdir "$@" \
		&& test "$PWD" != "$OLDPWD" \
		&& cd - >/dev/null
	}

	iterate_tests 3 "$@"
}

test_die() {
	set -- \
		eq    1    0  \
		eq    2    2  \
		eq  126  126  \
		eq  255  255

	callback() {
		local retval stderr

		test_description="( exit $2 ); die"
		( exit "$2" )
		stderr=$(die "$2" 2>&1)
		retval=$?
		if [ "${stderr}" = "test-functions: $2" ]; then
			return "${retval}"
		else
			return 1
		fi
	}

	iterate_tests 3 "$@"
}

test_ebegin() {
	set -- eq 0

	callback() {
		test_description="ebegin message (expecting terminating newline)"
		(
			_eprint() {
				shift
				_ends_with_newline "$*"
			}
			ebegin "message"
		)
	}

	iterate_tests 2 "$@"
}

test_edo() {
	set -- \
		eq  1  false \
		eq  0  true

	callback() {
		shift
		test_description="edo $1"
		( edo "$1" >/dev/null )
	}

	iterate_tests 3 "$@"
}

test_is_older_than() {
	local age tstamp

	set -- \
		ge  1  N/A           N/A           \
		ge  1  newer         N/A           \
		ge  1  newer-empty   N/A           \
		ge  1  newer/file    N/A           \
		ge  1  non-existent  N/A           \
		eq  0  newer         newer         \
		ge  1  newer         newer-empty   \
		eq  0  newer         newer/file    \
		ge  1  newer         non-existent  \
		ge  1  newer         older         \
		ge  1  newer         older-empty   \
		ge  1  newer         older/file    \
		eq  0  newer-empty   newer         \
		ge  1  newer-empty   newer-empty   \
		eq  0  newer-empty   newer/file    \
		ge  1  newer-empty   non-existent  \
		ge  1  newer-empty   older         \
		ge  1  newer-empty   older-empty   \
		ge  1  newer-empty   older/file    \
		ge  1  newer/file    newer         \
		ge  1  newer/file    newer-empty   \
		ge  1  newer/file    newer/file    \
		ge  1  newer/file    non-existent  \
		ge  1  newer/file    older         \
		ge  1  newer/file    older-empty   \
		ge  1  newer/file    older/file    \
		eq  0  non-existent  newer         \
		eq  0  non-existent  newer-empty   \
		eq  0  non-existent  newer/file    \
		ge  1  non-existent  non-existent  \
		eq  0  non-existent  older         \
		eq  0  non-existent  older-empty   \
		eq  0  non-existent  older/file    \
		eq  0  older         newer         \
		eq  0  older         newer-empty   \
		eq  0  older         newer/file    \
		ge  1  older         non-existent  \
		eq  0  older         older         \
		ge  1  older         older-empty   \
		eq  0  older         older/file    \
		eq  0  older-empty   newer         \
		eq  0  older-empty   newer-empty   \
		eq  0  older-empty   newer/file    \
		ge  1  older-empty   non-existent  \
		eq  0  older-empty   older         \
		ge  1  older-empty   older-empty   \
		eq  0  older-empty   older/file    \
		eq  0  older/file    newer         \
		eq  0  older/file    newer-empty   \
		eq  0  older/file    newer/file    \
		ge  1  older/file    non-existent  \
		ge  1  older/file    older         \
		ge  1  older/file    older-empty   \
		ge  1  older/file    older/file

	# The mtimes need to be explicitly assigned. Empirical evidence has
	# shown that executing mkdir(1) sequentially, with a single operand
	# each time, does not guarantee the order of the resulting mtimes.
	tstamp=197001010000
	for age in older newer; do
		mkdir "${age}" "${age}-empty" \
		&& touch -m -t "${tstamp%0}1" "${age}"/file \
		&& touch -m -t "${tstamp}" "${age}" "${age}-empty" \
		|| bailout "Couldn't create or adjust the mtimes of the sample files"
		tstamp=197001010100 # add an hour
	done

	callback() {
		shift
		test_description="is_older_than $(quote_args "$@")"
		is_older_than "$@"
	}

	iterate_tests 4 "$@"
}

test_get_bootparam() {
	local cmdline

	cmdline="foo gentoo=bar,baz quux"
	set -- \
		ge  1  "${cmdline}"                   N/A       \
		ge  1  "${cmdline}"                   ''        \
		ge  1  "gentoo="                      ''        \
		ge  1  "${cmdline}"                   foo       \
		eq  0  "${cmdline}"                   bar       \
		eq  0  "foo gentoo=gentoo=1,bar baz"  bar       \
		eq  0  "foo gentoo=bar,gentoo=1 baz"  bar       \
		eq  0  "${cmdline}"                   baz       \
		ge  1  "${cmdline}"                   bar,baz   \
		eq  0  "foo gentoo=bar,gentoo=1 baz"  gentoo=1  \
		eq  0  "foo gentoo=gentoo=1,bar baz"  gentoo=1  \
		ge  1  "${cmdline}"                   quux

	callback() {
		local cmdline

		cmdline=$2
		shift 2
		test_description="get_bootparam $(quote_args "$@")"
		printf '%s\n' "${cmdline}" | get_bootparam "$@"
	}

	iterate_tests 4 "$@"
}

test_esyslog() {
	set -- \
		ge  1  0  N/A    N/A   N/A      \
		ge  1  0  debug  N/A   N/A      \
		eq  0  0  debug  user  N/A      \
		eq  0  0  debug  user  ''       \
		eq  0  1  debug  user  message

	logger() {
		# esyslog() ignores empty messages. By overriding logger(1), it
		# can be determined whether a message would have been logged.
		printf '1\n'
	}

	callback() {
		local logged should_log

		should_log=$2
		shift 2
		test_description="esyslog $(quote_args "$@")"
		# shellcheck disable=2034
		logged=$(EINFO_LOG=1; esyslog "$@")
		case $? in
			0)
				test "${logged:-0}" -eq "${should_log}"
				;;
			*)
				return "$?"
		esac
	}

	iterate_tests 6 "$@"
}

test_is_identifier() {
	set -- \
		ge  1   ''   \
		ge  1    _   \
		ge  1    0   \
		ge  1   0a   \
		ge  1   0Z   \
		ge  1    9   \
		ge  1   9a   \
		ge  1   9Z   \
		ge  1   /a   \
		ge  1   /Z   \
		ge  1   .a   \
		ge  1   .Z   \
		ge  1  '[a'  \
		ge  1  '[Z'  \
		ge  1  '`a'  \
		ge  1  '`Z'  \
		ge  1  '{a'  \
		ge  1  '{Z'  \
		ge  1  '|a'  \
		ge  1  '|Z'  \
		ge  1   a/   \
		ge  1   Z/   \
		ge  1   a.   \
		ge  1   Z.   \
		ge  1  'a['  \
		ge  1  'Z['  \
		ge  1  'a`'  \
		ge  1  'Z`'  \
		ge  1  'a{'  \
		ge  1  'Z{'  \
		ge  1  'a|'  \
		ge  1  'Z|'  \
		eq  0    a   \
		eq  0    Z   \
		eq  0   __   \
		eq  0   _a   \
		eq  0   _Z   \
		eq  0   a_   \
		eq  0   Z_   \
		eq  0  a_a   \
		eq  0  a_Z   \
		eq  0  Z_a   \
		eq  0  Z_Z   \
		eq  0   a0   \
		eq  0   a9   \
		eq  0   Z0   \
		eq  0   Z9   \
		eq  0  a_0   \
		eq  0  a_9   \
		eq  0  Z_0   \
		eq  0  Z_9

	callback() {
		shift
		test_description="is_identifier $(quote_args "$@")"
		is_identifier "$@"
	}

	iterate_tests 3 "$@"
}

test_is_int() {
	set -- \
		ge  1  N/A        \
		ge  1  ' '        \
		ge  1  ' 1 '      \
		ge  1  ''         \
		ge  1  +1         \
		ge  1  +008       \
		ge  1  -008       \
		ge  1  008        \
		ge  1  x          \
		eq  0  0          \
		eq  0  1          \
		eq  0  -1         \
		eq  0  123456789

	callback() {
		shift
		test_description="is_int $(quote_args "$@")"
		is_int "$@"
	}

	iterate_tests 3 "$@"
}

test_is_visible() {
	set -- \
		ge  1  ''                   \
		ge  1  ' '                  \
		ge  1  "$(printf '\t')"     \
		ge  1  "$(printf '\a')"     \
		eq  0  .                    \
		eq  0  ' . '                \
		eq  0  "$(printf '\t.\t')"  \
		eq  0  "$(printf '\a.\a')"

	callback() {
		shift
		test_description="_is_visible $(quote_args "$@")"
		_is_visible "$@"
	}

	iterate_tests 3 "$@"
}

test_yesno() {
	set -- \
		eq  0  yes                                \
		eq  0  YES                                \
		eq  0  Yes                                \
		eq  0  true                               \
		eq  0  TRUE                               \
		eq  0  true                               \
		eq  0  on                                 \
		eq  0  ON                                 \
		eq  0  On                                 \
		eq  0  1                                  \
		eq  0  truthful_nameref                   \
		ge  1  no                                 \
		ge  1  NO                                 \
		ge  1  No                                 \
		ge  1  false                              \
		ge  1  FALSE                              \
		ge  1  False                              \
		ge  1  off                                \
		ge  1  OFF                                \
		ge  1  Off                                \
		ge  1  0                                  \
		ge  1  not_a_nameref                      \
		ge  1  not-a-valid-nameref                \
		ge  1  '_"; set -- yes # code injection'

	# shellcheck disable=2034
	truthful_nameref=yes

	callback() {
		shift
		test_description="yesno $(quote_args "$@")"
		yesno "$@"
	}

	iterate_tests 3 "$@"
}

test_srandom() {
	set -- \
		eq  0  \
		eq  0  \
		eq  0  \
		eq  0  \
		eq  0  \
		eq  0  \
		eq  0  \
		eq  0  \
		eq  0  \
		eq  0

	callback() {
		local number

		number=$(srandom)
		test_description="srandom ($(( row += 1 ))/10: ${number:-blank})"
		is_int "${number}" \
		&& awk -v "n=${number}" 'BEGIN { exit !(n >= 0 && n <= 2147483647) }'
	}

	row=0
	iterate_tests 2 "$@"
}

test_srandom_forked()
{
	set -- \
		eq  0  unforked  \
		eq  0  forking

	callback() {
		local mode number

		shift
		mode=$1
		set --
		test_description="srandom equality where $mode"
		if [ "${mode}" = "forking" ]; then
			srandom
			( srandom )
			srandom
			( srandom )
		else
			srandom
			srandom
			srandom
			srandom
		fi > random_numbers || return
		while read -r number; do
			set -- "$@" "${number}"
		done < random_numbers
		test_description="srandom equality where $mode ($*)"
		if [ "${mode}" = "forking" ]; then
			test "$#" -eq 4 && test "$1" -ne "$2" && test "$3" -ne "$4"
		else
			test "$#" -eq 4 && ! trueof_all test "$1" -eq -- "$@"
		fi
	}

	iterate_tests 3 "$@"
}

test_newest() {
	set -- \
		ge  1  non-existent  non-existent  \
		ge  1  N/A           N/A           \
		                                   \
		eq  0  newer/file    N/A           \
		eq  0  newer/file    newer/file    \
		eq  0  newer/file    non-existent  \
		eq  0  newer/file    older/file    \
		eq  0  newer/file    older/file    \
		eq  0  non-existent  newer/file    \
		eq  0  older/file    newer/file    \
		eq  0  older/file    newer/file    \
		                                   \
		eq  0  older/file    N/A           \
		eq  0  older/file    older/file    \
		eq  0  older/file    non-existent  \
		eq  0  non-existent  older/file    \

	callback() {
		shift
		test_description="newest $(quote_args "$@")"
		row=$(( row + 1 ))
		true |
		case ${row} in
			[1-2])
				newest "$@"
				;;
			[3-9]|10)
				test "$(newest "$@")" = "newer/file"
				;;
			*)
				test "$(newest "$@")" = "older/file"
		esac
	}

	row=0
	iterate_tests 4 "$@"

	callback() {
		shift
		if [ "$#" -eq 0 ]; then
			test_description=": | newest"
		else
			test_description="printf '%s\\0' $(quote_args "$@") | newest"
		fi
		row=$(( row + 1 ))
		{
			test "$#" -gt 0 && printf '%s\0' "$@"
		} |
		case ${row} in
			[1-2])
				newest
				;;
			[3-9]|10)
				test "$(newest)" = "newer/file"
				;;
			*)
				test "$(newest)" = "older/file"
		esac
	}

	row=0
	iterate_tests 4 "$@"
}

test_oldest() {
	set -- \
		ge  1  non-existent  non-existent  \
		ge  1  N/A           N/A           \
		                                   \
		eq  0  newer/file    N/A           \
		eq  0  newer/file    newer/file    \
		eq  0  newer/file    non-existent  \
		eq  0  non-existent  newer/file    \
		                                   \
		eq  0  newer/file    older/file    \
		eq  0  non-existent  older/file    \
		eq  0  older/file    N/A           \
		eq  0  older/file    newer/file    \
		eq  0  older/file    non-existent  \
		eq  0  older/file    older/file    \
		eq  0  newer/file    older/file    \
		eq  0  older/file    newer/file

	callback() {
		shift
		test_description="oldest $(quote_args "$@")"
		row=$((row + 1))
		true |
		case ${row} in
			[1-2])
				oldest "$@"
				;;
			[3-6])
				test "$(oldest "$@")" = "newer/file"
				;;
			*)
				test "$(oldest "$@")" = "older/file"
		esac
	}

	row=0
	iterate_tests 4 "$@"

	callback() {
		shift
		if [ "$#" -eq 0 ]; then
			test_description=": | oldest"
		else
			test_description="printf '%s\\0' $(quote_args "$@") | oldest"
		fi
		row=$(( row + 1 ))
		{
			test "$#" -gt 0 && printf '%s\0' "$@"
		} |
		case ${row} in
			[1-2])
				oldest
				;;
			[3-6])
				test "$(oldest)" = "newer/file"
				;;
			*)
				test "$(oldest)" = "older/file"
		esac
	}

	row=0
	iterate_tests 4 "$@"
}

test_trim() {
	set -- \
		eq  0  ''                       ''     \
		eq  0  ' '                      ''     \
		eq  0  '  '                     ''     \
		eq  0  ' X'                     'X'    \
		eq  0  '  X'                    'X'    \
		eq  0  'X  '                    'X'    \
		eq  0  ' X Y'                   'X Y'  \
		eq  0  '  X Y'                  'X Y'  \
		eq  0  'X Y '                   'X Y'  \
		eq  0  'X Y  '                  'X Y'  \
		eq  0  "$(printf ' \tX')"       'X'    \
		eq  0  "$(printf ' \tX\t ')"    'X'    \
		eq  0  "$(printf    'X\t ')"    'X'    \
		eq  0  "$(printf ' \tX Y')"     'X Y'  \
		eq  0  "$(printf ' \tX Y\t ')"  'X Y'  \
		eq  0  "$(printf    'X Y\t ')"  'X Y'

	callback() {
		shift
		test_description="trim $(quote_args "$1") (expecting $(quote_args "$2"))"
		test "$(trim "$1")" = "$2"
	}

	iterate_tests 4 "$@"
}

test_hr() {
	# shellcheck disable=2183
	set -- \
		eq  0  "$(printf '%80s' | tr ' ' -)"  N/A  N/A  \
		eq  0  "$(printf '%80s' | tr ' ' -)"  -    N/A  \
		eq  0  ''                             -    0    \
		eq  0  -                              -    1    \
		eq  0  -----------------              -    17   \
		eq  0  ''                             xyz  0    \
		eq  0  x                              xyz  1    \
		eq  0  xxxxxxxxxxxxxxxxx              xyz  17

	callback() {
		local expected

		shift
		expected=$1
		shift
		test_description="hr $(quote_args "$@")"
		test "$(hr "$@")" = "${expected}"
	}

	iterate_tests 5 "$@"
}

test_whenceforth() {
	set  --  \
		ge  1  PATH                        N/A                N/A                \
		ge  1  PATH                        .                  N/A                \
		ge  1  PATH                        unlikely-to-exist  N/A                \
		ge  1  PATH                        /var/empty         N/A                \
		ge  1  PATH                        /var/empty/nofile  N/A                \
		eq  0  PATH                        /bin/sh            N/A                \
		eq  0  PATH                        sh                 N/A                \
		ge  1  PATH                        -x                 .                  \
		ge  1  PATH                        -x                 unlikely-to-exist  \
		ge  1  PATH                        -x                 /var/empty         \
		ge  1  PATH                        -x                 /var/empty/nofile  \
		eq  0  PATH                        -x                 /bin/sh            \
		eq  0  PATH                        -x                 sh                 \
		eq  0  ''                          -x                 newer/file         \
		eq  0  .                           -x                 newer/file         \
		eq  0  :/var/empty/x               -x                 newer/file         \
		eq  0  /var/empty/x:               -x                 newer/file         \
		eq  0  /var/empty/x::/var/empty/y  -x                 newer/file         \
		eq  0  ''                          -x                 newer/file         \
		eq  0  .                           -x                 newer/file         \
		eq  0  :/var/empty/x               -x                 newer/file         \
		eq  0  /var/empty/x:               -x                 newer/file         \
		eq  0  /var/empty/x::/var/empty/y  -x                 newer/file         \
		eq  0  ''                          older/file         N/A                \
		eq  0  .                           older/file         N/A                \
		eq  0  :/var/empty/x               older/file         N/A                \
		eq  0  /var/empty/x:               older/file         N/A                \
		eq  0  /var/empty/x::/var/empty/y  older/file         N/A                \
		ge  1  ''                          -x                 older/file         \
		ge  1  .                           -x                 older/file         \
		ge  1  :/var/empty/x               -x                 older/file         \
		ge  1  /var/empty/x:               -x                 older/file         \
		ge  1  /var/empty/x::/var/empty/y  -x                 older/file

	chmod +x newer/file

	callback() {
		local path

		shift
		path=$1
		shift
		if [ "${path}" = PATH ]; then
			test_description="whenceforth $(quote_args "$@")"
			whenceforth "$@" >/dev/null
		else
			test_description="PATH=${path} whenceforth $(quote_args "$@")"
			(
				# If necessary, declare functions to cover the
				# utilities that might otherwise be unavailable
				# on account of the various values of PATH
				# being tested. It cannot be assumed that the
				# utilities in question are builtins.
				case ${printf_cmd} in
					/*) printf() { "${printf_cmd}" "$@"; }
				esac
				case ${test_cmd} in
					/*) test() { "${test_cmd}" "$@"; }
				esac
				# shellcheck disable=2030
				PATH=${path}
				whenceforth "$@" >/dev/null
			)
		fi
	}

	printf_cmd=$(command -v printf)
	test_cmd=$(command -v test)
	iterate_tests 5 "$@"
}

test_get_nprocs() {
	set -- eq 0

	callback() {
		local nproc

		shift
		test_description="get_nprocs"
		nproc=$(get_nprocs) && is_int "${nproc}" && test "${nproc}" -gt 0
	}

	iterate_tests 2 "$@"
}

test_parallel_run() {
	set -- \
		ge  1  N/A                     N/A                     \
		eq  0  /                       N/A                     \
		ge  1  /var/empty/nonexistent  N/A                     \
		eq  0  /                       /                       \
		ge  1  /                       /var/empty/nonexistent  \
		ge  1  /var/empty/nonexistent  /var/empty/nonexistent  \
		ge  1  /var/empty/nonexistent  /

	callback() {
		shift
		test_description="parallel_run $(quote_args 0 ls "$@")"
		parallel_run 0 ls "$@" >/dev/null 2>&1
	}

	iterate_tests 4 "$@"
}

test_is_anyof() {
	set -- \
		ge  1  N/A  N/A  N/A  \
		ge  1  x    N/A  N/A  \
		ge  1  x    y    N/A  \
		ge  1  x    y    z    \
		eq  0  x    x    N/A  \
		eq  0  x    x    y    \
		eq  0  x    y    x

	callback() {
		shift
		test_description="is_anyof $(quote_args "$@")"
		is_anyof "$@"
	}

	iterate_tests 5 "$@"
}

test_is_subset() {
	set -- \
		ge  1  N/A  N/A  N/A  N/A  N/A  \
		ge  1  --   N/A  N/A  N/A  N/A  \
		ge  1  --   --   N/A  N/A  N/A  \
		ge  1  --   x    N/A  N/A  N/A  \
		ge  1  x    --   N/A  N/A  N/A  \
		ge  1  x    y    N/A  N/A  N/A  \
		ge  1  x    y    x    N/A  N/A  \
		eq  0  x    --   x    N/A  N/A  \
		eq  0  x    --   x    y    N/A  \
		eq  0  x    --   y    x    N/A  \
		eq  0  x    y    --   x    y    \
		eq  0  x    y    --   y    x    \
		ge  1  x    y    --   x    z    \
		ge  1  y    x    --   z    x    \
		ge  1  x    z    --   x    y    \
		ge  1  z    x    --   y    x

	callback() {
		shift
		test_description="is_subset $(quote_args "$@")"
		is_subset "$@"
	}

	iterate_tests 7 "$@"
}

test_trueof_all() {
	set -- \
		ge  1  N/A   N/A  N/A  N/A        N/A        \
		ge  1  test  -d   N/A  N/A        N/A        \
		ge  1  test  -d   --   N/A        N/A        \
		ge  1  test  -d   --   /dev/null  N/A        \
		ge  1  test  -d   --   /dev/null  /dev/null  \
		eq  0  test  -d   --   /          N/A        \
		eq  0  test  -d   --   /          /          \
		ge  1  test  -d   --   /          /dev/null  \
		ge  1  test  -d   --   /dev/null  /

	callback() {
		shift
		test_description="trueof_all $(quote_args "$@")"
		trueof_all "$@"
	}

	iterate_tests 7 "$@"
}

test_trueof_any() {
	set -- \
		ge  1  N/A   N/A  N/A  N/A        N/A        \
		ge  1  test  -d   N/A  N/A        N/A        \
		ge  1  test  -d   --   N/A        N/A        \
		ge  1  test  -d   --   /dev/null  N/A        \
		ge  1  test  -d   --   /dev/null  /dev/null  \
		eq  0  test  -d   --   /          N/A        \
		eq  0  test  -d   --   /          /          \
		eq  0  test  -d   --   /          /dev/null  \
		eq  0  test  -d   --   /dev/null  /

	callback() {
		shift
		test_description="trueof_any $(quote_args "$@")"
		trueof_any "$@"
	}

	iterate_tests 7 "$@"
}

test_substr() {
	set -- \
		ge  1  -       foobar  N/A  N/A  \
		ge  1  -       foobar  ''   N/A  \
		ge  1  -       foobar  x    N/A  \
		ge  1  -       foobar  ''   ''   \
		ge  1  -       foobar  x    y    \
		eq  0  foobar  foobar  1    N/A  \
		eq  0  foobar  foobar  -1   N/A  \
		eq  0  foobar  foobar  1    7    \
		eq  0  foobar  foobar  -1   7    \
		eq  0  foo     foobar  1    3    \
		eq  0  foo     foobar  -1   3    \
		eq  0  f       foobar  1    1    \
		eq  0  f       foobar  -1   1    \
		eq  0  ''      foobar  1    0    \
		eq  0  ''      foobar  1    -1   \
		eq  0  ''      foobar  0    0    \
		eq  0  ''      foobar  0    -1   \
		eq  0  ''      foobar  -1   0    \
		eq  0  ''      foobar  -1   -1   \
		eq  0  bar     foobar  4    N/A  \
		eq  0  bar     foobar  4    4    \
		eq  0  b       foobar  4    1    \
		eq  0  ''      foobar  4    0    \
		eq  0  ''      foobar  4    -1

	callback() {
		local expected str

		shift
		expected=$1
		shift
		test_description="substr $(quote_args "$@")"
		str=$(substr "$@") && test "${str}" = "${expected}"
	}

	iterate_tests 6 "$@"
}

test_contains_all() {
	set -- \
		ge  1  N/A           N/A         N/A         N/A  \
		ge  1  ' foo  bar '  ''          N/A         N/A  \
		ge  1  ' foo  bar '  ''          ' '         N/A  \
		ge  1  ' foo  bar '  ''          ' bar'      N/A  \
		ge  1  ' foo  bar '  ''          'foo '      N/A  \
		ge  1  ' foo  bar '  ''          'foo  bar'  N/A  \
		ge  1  ' foo  bar '  ' '         ''          N/A  \
		ge  1  ' foo  bar '  ' '         ' '         N/A  \
		ge  1  ' foo  bar '  ' '         N/A         N/A  \
		ge  1  ' foo  bar '  ' bar'      ''          N/A  \
		ge  1  ' foo  bar '  ' bar'      N/A         N/A  \
		ge  1  ' foo  bar '  'foo '      ''          N/A  \
		ge  1  ' foo  bar '  'foo '      ' bar'      N/A  \
		ge  1  ' foo  bar '  'foo '      N/A         N/A  \
		ge  1  ' foo  bar '  'foo  bar'  ''          N/A  \
		ge  1  ' foo  bar '  'foo  bar'  N/A         N/A  \
		ge  1  ' foo  bar '  N/A         N/A         N/A  \
		ge  1  ' foo  bar '  bar         foo         ''   \
		ge  1  ' foo  bar '  bar         foo         ' '  \
		ge  1  ' foo  bar '  baz         bar         foo  \
		ge  1  ' foo  bar '  fo          ba          N/A  \
		ge  1  ' foo  bar '  foo         bar         ''   \
		ge  1  ' foo  bar '  foo         bar         ' '  \
		ge  1  ' foo  bar '  foo         bar         baz  \
		ge  1  ' foo  bar '  o           a           N/A  \
		ge  1  ' foo  bar '  oo          ar          N/A  \
		eq  0  ' foo  bar '  foo         bar         N/A  \
		eq  0  ' foo  bar '  bar         foo         N/A

	callback() {
		shift
		test_description="contains_all $(quote_args "$@")"
		contains_all "$@"
	}

	iterate_tests 6 "$@"
}

test_contains_any() {
	set -- \
		ge  1  N/A        N/A        N/A        \
		ge  1  'foo bar'  N/A        N/A        \
		ge  1  'foo bar'  fo         ba         \
		ge  1  'foo bar'  oo         ar         \
		ge  1  'foo bar'  o          a          \
		ge  1  'foo bar'  'foo bar'  'foo bar'  \
		ge  1  'foo bar'  'foo bar'  _          \
		ge  1  'foo bar'  _          'foo bar'  \
		ge  1  'foo bar'  'foo '     ' bar'     \
		ge  1  'foo bar'  'foo '     _          \
		ge  1  'foo bar'  _          ' bar'     \
		ge  1  'foo bar'  ' bar'     _          \
		ge  1  'foo bar'  _          'foo '     \
		ge  1  'foo bar'  ''         ''         \
		ge  1  'foo bar'  ''         _          \
		ge  1  'foo bar'  _          ''         \
		ge  1  'foo bar'  ' '        ' '        \
		ge  1  'foo bar'  ' '        _          \
		ge  1  'foo bar'  _          ' '        \
		eq  0  'foo bar'  foo        bar        \
		eq  0  'foo bar'  bar        foo        \
		eq  0  'foo bar'  foo        _          \
		eq  0  'foo bar'  _          bar        \
		eq  0  'foo bar'  bar        _          \
		eq  0  'foo bar'  _          foo

	callback() {
		shift
		test_description="contains_any $(quote_args "$@")"
		contains_any "$@"
	}

	iterate_tests 5 "$@"
}

test_quote_args() {
	set -- eq 0

	callback() {
		local POSIXLY_CORRECT cksum fmt i str

		test_description="quote_args output test (expecting cksum 380900690)"
		i=0
		# The generator fails to produce the correct ouput in yash
		# unless the effective character type is C/POSIX. However, once
		# launched, yash ignores assignments to the LC_CTYPE variable
		# if in its posix mode. As things stand, there is little point
		# in fixing it because yash also disables the local builtin in
		# its posix mode, causing test-functions to bail out sooner.
		while [ "$((i += 1))" -le 255 ]; do
			fmt=$(printf '\\%o' "$i")
			# shellcheck disable=2059
			str=$(printf "$fmt.")
			quote_args "${str%.}" || break
		done \
		| cksum \
		| { read -r cksum _ && test "${cksum}" = "380900690"; }
	}

	iterate_tests 2 "$@"
}

test_assign() {
	set -- \
		ge  1  N/A              N/A       \
		ge  1  ''               N/A       \
		ge  1  0                N/A       \
		ge  1  valid_nameref    N/A       \
		ge  1  ''               marmoset  \
		ge  1  0                marmoset  \
		ge  1  valid_nameref    N/A       \
		ge  1  'injection=1 #'  comment   \
		eq  0  valid_nameref    marmoset

	callback() {
		local injection

		shift
		test_description="assign $(quote_args "$@")"
		injection=
		assign "$@" 2>/dev/null || test "${injection}"
	}

	iterate_tests 4 "$@"
}

test_deref() {
	set -- \
		ge  1  N/A            N/A              \
		ge  1  ''             N/A              \
		ge  1  0              N/A              \
		ge  1  ''             ''               \
		ge  1  0              0                \
		eq  0  valid_nameref  N/A              \
		eq  0  valid_nameref  assignee         \
		ge  1  PWD            'injection=1 #'

	callback() {
		local assignee injection stdout

		shift
		test_description="deref $(quote_args "$@")"
		case $# in
			2)
				assignee= injection=
				deref "$@" \
				&& { test "${assignee}" = "marmoset" || test "${injection}"; }
				;;
			*)
				stdout=$(deref "$@") && test "${stdout}" = "marmoset"
				;;
		esac 2>/dev/null
	}

	iterate_tests 4 "$@"
}

test_update_time() {
	local locale

	# The yash shell dies upon integer overflow and _update_time() ends up
	# being deactivated for it. Hence, there is no reason to run this test.
	if [ "${YASH_VERSION}" ]; then
		return
	fi

	set -- \
		de_BE de_DE es_ES fr_BE fr_CA fr_FR it_IT nl_BE nl_NL pl_PL \
		pt_BR pt_PT ru_RU sv_SE

	# Try to test a locale for which the radix character isn't U+2E.
	locale=$(
		IFS='|'
		locale -a 2>/dev/null | awk "/^($*)\.(utf8|UTF-8)$/ { print; exit }"
	)
	if [ "${locale}" ]; then
		set -- eq 0 "$locale"
	else
		set --
	fi

	# Also test the currently effective locale, whichever it may be.
	set -- "$@" eq 0 ''

	callback() {
		local genfun_time

		shift
		if [ "$1" ]; then
			test_description="LC_ALL=$1 _update_time"
			genfun_time=$(LC_ALL=$1; _update_time && printf %s "${genfun_time}")
		else
			test_description="_update_time"
			genfun_time=$(_update_time && printf %s "${genfun_time}")
		fi
		case $? in
			0)
				is_int "${genfun_time}"
				;;
			2)
				# Unsupported for the platform and therefore untestable.
				true
				;;
			*)
				false
		esac
	}

	iterate_tests 3 "$@"
}

test_should_throttle() {
	local bits max_int

	# The yash shell dies upon integer overflow and _update_time() ends up
	# being deactivated for it. Hence, there is no reason to run this test.
	if [ "${YASH_VERSION}" ]; then
		return
	fi

	genfun_time=
	bits=30
	while [ "${bits}" -lt 128 ]; do
		# Dash is buggy and fails to handle $(( 1 << ++bits )).
		bits=$(( bits + 1 ))
		case $(( max_int = 1 << bits )) in
			-*)
				max_int=$(( max_int - 1 ))
				genfun_time=$(( max_int - 4 ))
				break
		esac
	done

	if [ ! "${genfun_time}" ]; then
		bailout "Failed to calculate the maximum possible integer value"
	fi

	# For the first test, genfun_last_time is not yet known. Therefore, the
	# return value should always be 1. For the fifth test, integer overflow
	# is expected to occur. Again, the return value should always be 1.
	set -- \
		ge  1  "${max_int}"  \
		eq  0  2             \
		ge  1  1             \
		ge  1  0             \
		ge  1  2             \
		eq  0  2             \
		ge  1  1             \
		ge  1  0

	_update_time() {
		true
	}

	callback() {
		shift
		test_description="_should_throttle $1 (${genfun_time}, $(( genfun_time += 1 )))"
		_should_throttle "$1"
	}

	iterate_tests 3 "$@"
}

iterate_tests() {
	local code i j passed slice_width total

	slice_width=$1
	shift
	total=$(( $# / slice_width ))
	passed=0
	i=0
	while [ "$((i += 1))" -le "${total}" ]; do
		code="callback"
		j=1
		while [ "$((j += 1))" -le "${slice_width}" ]; do
			if eval "[ \"\$${j}\" = N/A ]"; then
				break
			else
				code="${code} \"\$${j}\""
			fi
		done
		eval "${code}"
		retval=$?
		if test "${retval}" -"$1" "$2"; then
			passed=$((passed + 1))
		else
			printf 'not '
		fi
		printf 'ok %d - %s (test %d -%s %d)\n' \
			"$((testnum += 1))" "${test_description}" "${retval}" "$1" "$2"
		shift "${slice_width}"
	done
	return "$(( passed < total ))"
}

printf 'TAP version 13\n'

unset -v global_tmpdir

# PATH is redefined to prevent ebuild-helpers such as die from interfering.
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/opt/pkg/bin
export TEST_GENFUNCS=1
export TZ=UTC
testnum=0
rc=0

if [ "${PORTAGE_BIN_PATH}" ] && [ "${S}" ]; then
	# shellcheck disable=2034
	genfun_basedir=${S}
fi

if ! test_local; then
	# Currently, this test is known to fail for ksh93 and yash. As regards
	# the former, the commonly implemented behaviour of "local" can be
	# approximated with "typeset". However, to use typeset in this way
	# requires the use of the function f { ...; } syntax instead of the
	# POSIX-compatible f() compound-command syntax. Further, ksh93
	# implements static scoping. As regards the latter, yash is rather
	# stringent and simply disables its local builtin if in its posix mode.
	# Running yash as "sh" would be one way of activating said mode.
	rc=1
elif ! GENFUN_MODULES="portage rc" . ./functions.sh; then
	bailout "Couldn't source ./functions.sh"
else
	assign_tmpdir
	test_chdir || rc=1
	test_ebegin || rc=1
	test_is_older_than || rc=1
	test_get_bootparam || rc=1
	test_esyslog || rc=1
	test_is_identifier || rc=1
	test_is_int || rc=1
	test_is_visible || rc=1
	test_yesno || rc=1
	test_die || rc=1
	test_edo || rc=1
	if ! test_srandom; then
		rc=1
	else
		test_srandom_forked || rc=1
	fi
	test_newest || rc=1
	test_oldest || rc=1
	test_trim || rc=1
	test_hr || rc=1
	test_whenceforth || rc=1
	test_parallel_run || rc=1
	test_is_anyof || rc=1
	#test_is_subset || rc=1
	test_trueof_all || rc=1
	test_trueof_any || rc=1
	#test_substr || rc=1
	test_contains_all || rc=1
	test_contains_any || rc=1
	test_quote_args || rc=1
	test_assign || rc=1
	test_deref || rc=1
	test_update_time || rc=1
	test_should_throttle || rc=1
fi

cleanup_tmpdir

printf '1..%d\n' "${testnum}"

exit "${rc}"
