diff --git a/.github/actions/build-and-tag-locally/action.yml b/.github/actions/build-and-tag-locally/action.yml
index 70c2a052f..f67023fd5 100644
--- a/.github/actions/build-and-tag-locally/action.yml
+++ b/.github/actions/build-and-tag-locally/action.yml
@@ -204,6 +204,26 @@ runs:
exit 1
fi
+ - name: Test the entrypoint
+ id: test_entrypoint
+ if: ${{ contains(fromJSON('["amd64", "i386"]'), steps.platform.outputs.display_name) }}
+ shell: bash
+ run: >
+ cd test && env
+ PLATFORM=${{ steps.platform.outputs.display_name }}
+ REDIS_IMG=${{ github.sha }}:${{ steps.platform.outputs.display_name }}
+ ./run-entrypoint-tests.sh
+ -- --output-junit-xml=report-entrypoint.xml
+
+ - name: Test Report
+ uses: dorny/test-reporter@v2
+ # run this step even if previous step failed, but not if it was skipped
+ if: ${{ !cancelled() && steps.test_entrypoint.conclusion != 'skipped' }}
+ with:
+ name: Entrypoint Tests
+ path: test/report-entrypoint.xml
+ reporter: java-junit
+
- name: Push image
uses: docker/build-push-action@v6
if: ${{ inputs.publish_image == 'true' && contains(fromJSON('["amd64"]'), steps.platform.outputs.display_name) }}
@@ -212,4 +232,4 @@ runs:
push: true
tags: ${{ inputs.registry_repository }}:${{ github.sha }}-${{ inputs.distribution }}
cache-from: type=gha
- cache-to: type=gha,mode=max
\ No newline at end of file
+ cache-to: type=gha,mode=max
diff --git a/alpine/docker-entrypoint.sh b/alpine/docker-entrypoint.sh
index ab5befbf8..476913d93 100755
--- a/alpine/docker-entrypoint.sh
+++ b/alpine/docker-entrypoint.sh
@@ -1,8 +1,93 @@
#!/bin/sh
set -e
+SETPRIV="/usr/bin/setpriv --reuid redis --regid redis --clear-groups"
+IS_REDIS_SENTINEL=""
+IS_REDIS_SERVER=""
+CONFIG=""
+
+SKIP_FIX_PERMS_NOTICE="Use SKIP_FIX_PERMS=1 to skip permission changes."
+
+# functions
has_cap() {
- /usr/bin/setpriv -d | grep -q 'Capability bounding set:.*\b'$1'\b'
+ /usr/bin/setpriv -d | grep -q 'Capability bounding set:.*\b'"$1"'\b'
+}
+
+check_for_sentinel() {
+ CMD="$1"
+ shift
+ if [ "$CMD" = '/usr/local/bin/redis-server' ]; then
+ for arg in "$@"; do
+ if [ "$arg" = "--sentinel" ]; then
+ return 0
+ fi
+ done
+ fi
+
+ if [ "$CMD" = '/usr/local/bin/redis-sentinel' ]; then
+ return 0
+ fi
+
+ return 1
+}
+
+# Note: Change permissions only in simple, default cases to avoid affecting
+# unexpected or user-specific files.
+
+fix_data_dir_perms() {
+ # Expecting only *.rdb files and default appendonlydir; skip if others are found.
+ unknown_file="$(find . -mindepth 1 -maxdepth 1 \
+ -not \( -name \*.rdb -or \( -type d -and -name appendonlydir \) \) \
+ -print -quit)"
+ if [ -z "$unknown_file" ]; then
+ find . -print0 | fix_perms_and_owner rw
+ else
+ echo "Notice: Unknown file '$unknown_file' found in data dir. Permissions will not be modified. $SKIP_FIX_PERMS_NOTICE"
+ fi
+}
+
+fix_config_perms() {
+ config="$1"
+ mode="$2"
+
+ if [ ! -f "$config" ]; then
+ return 0
+ fi
+
+ confdir="$(dirname "$config")"
+ if [ ! -d "$confdir" ]; then
+ return 0
+ fi
+
+ # Expecting only the config file; skip if others are found.
+ pattern=$(printf "%s" "$(basename "$config")" | sed 's/[][?*]/\\&/g')
+ unknown_file=$(find "$confdir" -mindepth 1 -maxdepth 1 -not -name "$pattern" -print -quit)
+
+ if [ -z "$unknown_file" ]; then
+ printf '%s\0%s\0' "$confdir" "$config" | fix_perms_and_owner "$mode"
+ else
+ echo "Notice: Unknown file '$unknown_file' found in '$confdir'. Permissions will not be modified. $SKIP_FIX_PERMS_NOTICE"
+
+ fi
+}
+
+fix_perms_and_owner() {
+ mode="$1"
+
+ # shellcheck disable=SC3045
+ while IFS= read -r -d '' file; do
+ if [ "$mode" = "rw" ] && $SETPRIV test -r "$file" -a -w "$file"; then
+ continue
+ elif [ "$mode" = "r" ] && $SETPRIV test -r "$file"; then
+ continue
+ fi
+ new_mode=$mode
+ if [ -d "$file" ]; then
+ new_mode=${mode}x
+ fi
+ err=$(chown redis "$file" 2>&1) || echo "Warning: cannot change owner to 'redis' for '$file': $err. $SKIP_FIX_PERMS_NOTICE"
+ err=$(chmod "u+$new_mode" "$file" 2>&1) || echo "Warning: cannot change mode to 'u+$new_mode' for '$file': $err. $SKIP_FIX_PERMS_NOTICE"
+ done
}
# first arg is `-f` or `--some-option`
@@ -10,23 +95,43 @@ has_cap() {
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
set -- redis-server "$@"
fi
+CMD=$(command -v "$1" 2>/dev/null || :)
-CMD=$(realpath $(command -v "$1") 2>/dev/null || :)
-# drop privileges only if our uid is 0 (container started without explicit --user)
+if [ "$(readlink -f "$CMD")" = '/usr/local/bin/redis-server' ]; then
+ IS_REDIS_SERVER=1
+fi
+
+if check_for_sentinel "$CMD" "$@"; then
+ IS_REDIS_SENTINEL=1
+fi
+
+# if is server and its first arg is not an option then it's a config
+if [ "$IS_REDIS_SERVER" ] && [ "${2#-}" = "$2" ]; then
+ CONFIG="$2"
+fi
+
+# drop privileges only if
+# we are starting either server or sentinel
+# our uid is 0 (container started without explicit --user)
# and we have capabilities required to drop privs
-if has_cap setuid && has_cap setgid && \
- [ \( "$CMD" = '/usr/local/bin/redis-server' -o "$CMD" = '/usr/local/bin/redis-sentinel' \) -a "$(id -u)" = '0' ]; then
- find . \! -user redis -exec chown redis '{}' +
+if [ "$IS_REDIS_SERVER" ] && [ -z "$SKIP_DROP_PRIVS" ] && [ "$(id -u)" = '0' ] && has_cap setuid && has_cap setgid; then
+ if [ -z "$SKIP_FIX_PERMS" ]; then
+ # fix permissions
+ if [ "$IS_REDIS_SENTINEL" ]; then
+ fix_config_perms "$CONFIG" rw
+ else
+ fix_data_dir_perms
+ fix_config_perms "$CONFIG" r
+ fi
+ fi
+
CAPS_TO_KEEP=""
if has_cap sys_resource; then
# we have sys_resource capability, keep it available for redis
# as redis may use it to increase open files limit
CAPS_TO_KEEP=",+sys_resource"
fi
- exec /usr/bin/setpriv \
- --reuid redis \
- --regid redis \
- --clear-groups \
+ exec $SETPRIV \
--nnp \
--inh-caps=-all$CAPS_TO_KEEP \
--ambient-caps=-all$CAPS_TO_KEEP \
@@ -42,7 +147,7 @@ if [ "$um" = '0022' ]; then
umask 0077
fi
-if [ "$1" = 'redis-server' ]; then
+if [ "$IS_REDIS_SERVER" ] && ! [ "$IS_REDIS_SENTINEL" ]; then
echo "Starting Redis Server"
modules_dir="/usr/local/lib/redis/modules/"
@@ -76,5 +181,4 @@ if [ "$1" = 'redis-server' ]; then
fi
fi
-
exec "$@"
diff --git a/debian/docker-entrypoint.sh b/debian/docker-entrypoint.sh
index ab5befbf8..d0a21fe4d 100755
--- a/debian/docker-entrypoint.sh
+++ b/debian/docker-entrypoint.sh
@@ -1,8 +1,93 @@
-#!/bin/sh
+#!/bin/bash
set -e
+SETPRIV="/usr/bin/setpriv --reuid redis --regid redis --clear-groups"
+IS_REDIS_SENTINEL=""
+IS_REDIS_SERVER=""
+CONFIG=""
+
+SKIP_FIX_PERMS_NOTICE="Use SKIP_FIX_PERMS=1 to skip permission changes."
+
+# functions
has_cap() {
- /usr/bin/setpriv -d | grep -q 'Capability bounding set:.*\b'$1'\b'
+ /usr/bin/setpriv -d | grep -q 'Capability bounding set:.*\b'"$1"'\b'
+}
+
+check_for_sentinel() {
+ CMD="$1"
+ shift
+ if [ "$CMD" = '/usr/local/bin/redis-server' ]; then
+ for arg in "$@"; do
+ if [ "$arg" = "--sentinel" ]; then
+ return 0
+ fi
+ done
+ fi
+
+ if [ "$CMD" = '/usr/local/bin/redis-sentinel' ]; then
+ return 0
+ fi
+
+ return 1
+}
+
+# Note: Change permissions only in simple, default cases to avoid affecting
+# unexpected or user-specific files.
+
+fix_data_dir_perms() {
+ # Expecting only *.rdb files and default appendonlydir; skip if others are found.
+ unknown_file="$(find . -mindepth 1 -maxdepth 1 \
+ -not \( -name \*.rdb -or \( -type d -and -name appendonlydir \) \) \
+ -print -quit)"
+ if [ -z "$unknown_file" ]; then
+ find . -print0 | fix_perms_and_owner rw
+ else
+ echo "Notice: Unknown file '$unknown_file' found in data dir. Permissions will not be modified. $SKIP_FIX_PERMS_NOTICE"
+ fi
+}
+
+fix_config_perms() {
+ config="$1"
+ mode="$2"
+
+ if [ ! -f "$config" ]; then
+ return 0
+ fi
+
+ confdir="$(dirname "$config")"
+ if [ ! -d "$confdir" ]; then
+ return 0
+ fi
+
+ # Expecting only the config file; skip if others are found.
+ pattern=$(printf "%s" "$(basename "$config")" | sed 's/[][?*]/\\&/g')
+ unknown_file=$(find "$confdir" -mindepth 1 -maxdepth 1 -not -name "$pattern" -print -quit)
+
+ if [ -z "$unknown_file" ]; then
+ printf '%s\0%s\0' "$confdir" "$config" | fix_perms_and_owner "$mode"
+ else
+ echo "Notice: Unknown file '$unknown_file' found in '$confdir'. Permissions will not be modified. $SKIP_FIX_PERMS_NOTICE"
+
+ fi
+}
+
+fix_perms_and_owner() {
+ mode="$1"
+
+ # shellcheck disable=SC3045
+ while IFS= read -r -d '' file; do
+ if [ "$mode" = "rw" ] && $SETPRIV test -r "$file" -a -w "$file"; then
+ continue
+ elif [ "$mode" = "r" ] && $SETPRIV test -r "$file"; then
+ continue
+ fi
+ new_mode=$mode
+ if [ -d "$file" ]; then
+ new_mode=${mode}x
+ fi
+ err=$(chown redis "$file" 2>&1) || echo "Warning: cannot change owner to 'redis' for '$file': $err. $SKIP_FIX_PERMS_NOTICE"
+ err=$(chmod "u+$new_mode" "$file" 2>&1) || echo "Warning: cannot change mode to 'u+$new_mode' for '$file': $err. $SKIP_FIX_PERMS_NOTICE"
+ done
}
# first arg is `-f` or `--some-option`
@@ -10,23 +95,43 @@ has_cap() {
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
set -- redis-server "$@"
fi
+CMD=$(command -v "$1" 2>/dev/null || :)
-CMD=$(realpath $(command -v "$1") 2>/dev/null || :)
-# drop privileges only if our uid is 0 (container started without explicit --user)
+if [ "$(readlink -f "$CMD")" = '/usr/local/bin/redis-server' ]; then
+ IS_REDIS_SERVER=1
+fi
+
+if check_for_sentinel "$CMD" "$@"; then
+ IS_REDIS_SENTINEL=1
+fi
+
+# if is server and its first arg is not an option then it's a config
+if [ "$IS_REDIS_SERVER" ] && [ "${2#-}" = "$2" ]; then
+ CONFIG="$2"
+fi
+
+# drop privileges only if
+# we are starting either server or sentinel
+# our uid is 0 (container started without explicit --user)
# and we have capabilities required to drop privs
-if has_cap setuid && has_cap setgid && \
- [ \( "$CMD" = '/usr/local/bin/redis-server' -o "$CMD" = '/usr/local/bin/redis-sentinel' \) -a "$(id -u)" = '0' ]; then
- find . \! -user redis -exec chown redis '{}' +
+if [ "$IS_REDIS_SERVER" ] && [ -z "$SKIP_DROP_PRIVS" ] && [ "$(id -u)" = '0' ] && has_cap setuid && has_cap setgid; then
+ if [ -z "$SKIP_FIX_PERMS" ]; then
+ # fix permissions
+ if [ "$IS_REDIS_SENTINEL" ]; then
+ fix_config_perms "$CONFIG" rw
+ else
+ fix_data_dir_perms
+ fix_config_perms "$CONFIG" r
+ fi
+ fi
+
CAPS_TO_KEEP=""
if has_cap sys_resource; then
# we have sys_resource capability, keep it available for redis
# as redis may use it to increase open files limit
CAPS_TO_KEEP=",+sys_resource"
fi
- exec /usr/bin/setpriv \
- --reuid redis \
- --regid redis \
- --clear-groups \
+ exec $SETPRIV \
--nnp \
--inh-caps=-all$CAPS_TO_KEEP \
--ambient-caps=-all$CAPS_TO_KEEP \
@@ -42,7 +147,7 @@ if [ "$um" = '0022' ]; then
umask 0077
fi
-if [ "$1" = 'redis-server' ]; then
+if [ "$IS_REDIS_SERVER" ] && ! [ "$IS_REDIS_SENTINEL" ]; then
echo "Starting Redis Server"
modules_dir="/usr/local/lib/redis/modules/"
@@ -76,5 +181,4 @@ if [ "$1" = 'redis-server' ]; then
fi
fi
-
exec "$@"
diff --git a/test/run-entrypoint-tests.sh b/test/run-entrypoint-tests.sh
new file mode 100755
index 000000000..500fc3cec
--- /dev/null
+++ b/test/run-entrypoint-tests.sh
@@ -0,0 +1,649 @@
+#!/bin/bash
+
+##
+#
+# These tests are designed to verify the correctness of the entrypoint behavior
+# under different preconditions and arguments. As such, in some tests, it is
+# expected that the Redis process may fail with errors.
+#
+# To run specific test use:
+#
+# REDIS_IMG=image ./test.sh -- specific_test_name
+#
+# To get verbose output use TEST_VERBOSE=1:
+#
+# TEST_VERBOSE=1 REDIS_IMG=image ./test.sh
+#
+# Uses shunit2: https://github.com/kward/shunit2
+#
+# Requires sudo
+#
+##
+
+if [ -z "$REDIS_IMG" ]; then
+ echo "REDIS_IMG may not be empty"
+ exit 1
+fi
+# By default create files owned by root to avoid intersecting with container user
+HOST_UID=0
+HOST_GID=0
+if docker info 2>/dev/null | grep -qi rootless; then
+ # For rootless docker we have to use current user
+ HOST_UID=$(id -u)
+ HOST_GID=$(id -g)
+fi
+HOST_OWNER=$HOST_UID:$HOST_GID
+
+get_container_user_uid_gid_on_the_host() {
+ container_user="$1"
+ dir=$(mktemp -d -p .)
+ docker run --rm -v "$(pwd)/$dir":/w -w /w --entrypoint=/bin/sh "$REDIS_IMG" -c "chown $container_user ."
+ stat -c "%u %g" "$dir"
+ sudo rm -rf "$dir"
+}
+
+# Detect how redis user and group from the container are mapped to the host ones
+read -r REDIS_UID _ <<< "$(get_container_user_uid_gid_on_the_host redis:redis)"
+
+if [ "$REDIS_UID" == "$HOST_UID" ]; then
+ echo "Cannot test ownership as redis user uid is the same as current user"
+ exit 1
+fi
+
+# Helper functions #
+
+# creates one entry of directory structure
+# used in combination with iterate_dir_structure_with
+create_entry() {
+ dir="$1"
+ if [ "$type" = dir ]; then
+ sudo mkdir -p "$dir/$entry"
+ elif [ "$type" = file ]; then
+ sudo touch "$dir/$entry"
+ else
+ echo "Unknown type '$type' for entry '$entry'"
+ return 1
+ fi
+ sudo chmod "$initial_mode" "$dir/$entry"
+ sudo chown "$initial_owner" "$dir/$entry"
+}
+
+# asserts ownership and permissions for one entry from directory structure
+# used in combination with iterate_dir_structure_with
+assert_entry() {
+ dir="$1"
+ msg="$2"
+ actual_uid=$(sudo stat -c %u "$dir/$entry")
+ actual_mode=0$(sudo stat -c '%a' "$dir/$entry")
+ actual_mask=$(printf "0%03o" $(( actual_mode & expected_mode_mask )))
+ assertEquals "$msg: Owner for $type '$entry'" "$expected_owner" "$actual_uid"
+ assertEquals "$msg: Mode mask for $type '$entry'" "$expected_mode_mask" "$actual_mask"
+}
+
+# Iterates over directory structure assigning variables and executing command
+# from the arguments for each entry.
+#
+# Directory structure is the following form:
+# entry type initial owner -> expected uid initial mode -> expected mode mask
+# . dir $HOST_OWNER -> $REDIS_UID 0555 -> 0700
+# appendonlydir dir $HOST_OWNER -> $REDIS_UID 0333 -> 0600
+# dump.rdb file $HOST_OWNER -> $REDIS_UID 0333 -> 0600
+iterate_dir_structure_with() {
+ awk 'NF {print $1,$2,$3,$5,$6,$8}' \
+ | while read -r \
+ entry \
+ type \
+ initial_owner \
+ expected_owner \
+ initial_mode \
+ expected_mode_mask; \
+ do
+ "$@"
+ done
+}
+
+# Ownership and permissions test helper.
+#
+# This function tests the entrypoint.
+#
+# The idea is to test data and config files ownerhsip and permissions before and after container has started.
+#
+# The function creates temporary directory and uses --dir-structure (see iterate_dir_structure_with and create_entry)
+# to create files and directories in this temporary dir.
+#
+# The temporary dir is mounted into the --mount-target inside the container.
+#
+# Container is started using REDIS_IMG and the function arguments as CMD.
+#
+# After container exits, all file permissions and ownership are checked using expected values from --dir-structure (see assert_entry)
+#
+# Additionally --extra-assert function is invoked if present.
+#
+# Arguments:
+# < --mount-target DIR >
+# [ --dir-structure STRING ]
+# [ --extra-assert FUNCTION ]
+# [ --docker-flags FLAGS ]
+#
+# Positional arguments:
+# $docker_cmd
+run_docker_and_test_ownership() {
+ docker_flags=
+ extra_assert=
+ dir_structure=
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --dir-structure)
+ dir_structure="$2"
+ shift 2
+ ;;
+ --mount-target)
+ mount_target="$2"
+ shift 2
+ ;;
+ --docker-flags)
+ docker_flags="$2"
+ shift 2
+ ;;
+ --extra-assert)
+ extra_assert="$2"
+ shift 2
+ ;;
+ --*)
+ break
+ ;;
+ *)
+ break
+ ;;
+ esac
+ done
+ docker_cmd="$*"
+
+ if [ -z "$mount_target" ]; then
+ fail "Mount target is empty"
+ return 1
+ fi
+
+ dir=$(mktemp -d -p .)
+
+ iterate_dir_structure_with create_entry "$dir" <<<"$dir_structure"
+
+ docker_run="docker run --rm -v "$(pwd)/$dir":$mount_target $docker_flags $REDIS_IMG $docker_cmd"
+ if [ "$TEST_VERBOSE" ]; then
+ echo -e "\n#### ownership test: $docker_cmd"
+ echo "running $docker_run"
+ echo "Before:"
+ sudo find "$dir" -exec ls -ald {} \+
+ fi
+
+ docker_output=$($docker_run 2>&1)
+
+ if [ "$TEST_VERBOSE" ]; then
+ echo "After:"
+ sudo find "$dir" -exec ls -ald {} \+
+ echo "Docker output:"
+ echo "$docker_output"
+ fi
+
+ iterate_dir_structure_with assert_entry "$dir" "$docker_cmd" <<<"$dir_structure"
+
+ if [ "$extra_assert" ]; then
+ $extra_assert
+ fi
+
+ sudo rm -rf "$dir"
+}
+
+# running redis-server using different forms
+# -v option will make redis-server to either return version or fail (if config has been provided)
+# either one is OK for us
+run_docker_and_test_ownership_with_common_flags_for_server() {
+ run_docker_and_test_ownership "${common_flags[@]}" "$@" -v
+ run_docker_and_test_ownership "${common_flags[@]}" redis-server "$@" -v
+ run_docker_and_test_ownership "${common_flags[@]}" /usr/local/bin/redis-server "$@" -v
+}
+
+# running redis-sentinel using different forms and --dumb-option
+# expecting sentinel to fail, it's ok as we are only interested in entrypoint testing here
+run_docker_and_test_ownership_with_common_flags_for_sentinel() {
+ run_docker_and_test_ownership "${common_flags[@]}" "$@" --sentinel --dumb-option
+ run_docker_and_test_ownership "${common_flags[@]}" redis-sentinel "$@" --dumb-option
+ run_docker_and_test_ownership "${common_flags[@]}" /usr/local/bin/redis-sentinel "$@" --dumb-option
+ run_docker_and_test_ownership "${common_flags[@]}" redis-server "$@" --sentinel --dumb-option
+ run_docker_and_test_ownership "${common_flags[@]}" /usr/local/bin/redis-server "$@" --sentinel --dumb-option
+}
+
+# start redis server or sentinel and check process uid and gid
+run_redis_docker_and_check_uid_gid() {
+ docker_flags=
+ expected_cmd="redis-server"
+ user=redis
+ group=redis
+ file_owner=
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --user)
+ user="$2"
+ shift 2
+ ;;
+ --group)
+ group="$2"
+ shift 2
+ ;;
+ --expected-cmd)
+ expected_cmd="$2"
+ shift 2
+ ;;
+ --docker-flags)
+ docker_flags=$2
+ shift 2
+ ;;
+ --file-owner)
+ file_owner="$2"
+ shift 2
+ ;;
+ --*)
+ fail "Unknown flag $1"
+ return 1
+ ;;
+ *)
+ break
+ ;;
+ esac
+ done
+
+ if echo "$expected_cmd" | grep -q "sentinel"; then
+ dir="$(readlink -f "$(mktemp -d -p .)")"
+ touch "$dir/sentinel.conf"
+ if [ "$file_owner" ]; then
+ sudo chown -R "$file_owner" "$dir"
+ fi
+ docker_flags="-v $dir:/etc/sentinel $docker_flags"
+ fi
+
+ docker_cmd="$*"
+ # shellcheck disable=SC2086
+ container=$(docker run $docker_flags -d "$REDIS_IMG" $docker_cmd)
+ ret=$?
+
+ assertTrue "Container '$docker_flags $REDIS_IMG $docker_cmd' created" "[ $ret -eq 0 ]"
+ if [ $ret -gt 0 ]; then
+ echo "retarning"
+ return 1
+ fi
+
+ cmdline=$(docker exec "$container" cat /proc/1/cmdline|tr -d \\0)
+ assertContains "$docker_flags $docker_cmd, cmdline: $cmdline" "$cmdline" "$expected_cmd"
+
+ redis_user_uid=$(docker exec "$container" id -u "$user")
+ redis_user_gid=$(docker exec "$container" id -g "$group")
+
+ status=$(docker exec "$container" cat /proc/1/status)
+ process_uid=$(echo "$status" | grep Uid | cut -f2)
+ process_gid=$(echo "$status" | grep Gid | cut -f2)
+
+ assertEquals "redis cmd '$docker_cmd', process uid" "$redis_user_uid" "$process_uid"
+ assertEquals "redis cmd '$docker_cmd', process gid" "$redis_user_gid" "$process_gid"
+
+ docker stop "$container" >/dev/null
+ if [ "$dir" ]; then
+ sudo rm -rf "$dir"
+ fi
+}
+
+run_redis_docker_and_check_modules() {
+ docker_cmd="$1"
+ # shellcheck disable=SC2086
+ container=$(docker run --rm -d "$REDIS_IMG" $docker_cmd)
+ info=$(docker exec "$container" redis-cli info)
+
+ [ "$PLATFORM" ] && [ "$PLATFORM" != "amd64" ] && startSkipping
+ assertContains "$info" "module:name=timeseries"
+ assertContains "$info" "module:name=search"
+ assertContains "$info" "module:name=bf"
+ assertContains "$info" "module:name=vectorset"
+ assertContains "$info" "module:name=ReJSON"
+
+ docker stop "$container" >/dev/null
+}
+
+# helper assert function to check redis output
+assert_redis_output_has_no_config_perm_error() {
+ s="can't open config file"
+ assertNotContains "cmd: $docker_cmd, docker output contains '$s': " "$docker_output" "$s"
+}
+
+assert_redis_v8() {
+ assertContains "$1" "Redis server v=8"
+}
+
+# Tests #
+
+test_redis_version() {
+ ret=$(docker run --rm "$REDIS_IMG" -v|tail -n 1)
+ assert_redis_v8 "$ret"
+}
+
+test_data_dir_owner_and_perms_changed_by_server_when_data_is_RO() {
+ dir_structure="
+ . dir $HOST_OWNER -> $REDIS_UID 0555 -> 0700
+ appendonlydir dir $HOST_OWNER -> $REDIS_UID 0333 -> 0600
+ dump.rdb file $HOST_OWNER -> $REDIS_UID 0333 -> 0600
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /data)
+ run_docker_and_test_ownership_with_common_flags_for_server
+}
+
+test_data_dir_owner_and_perms_changed_by_server_when_appendonlydir_contains_files() {
+ dir_structure="
+ . dir $HOST_OWNER -> $REDIS_UID 0555 -> 0700
+ appendonlydir dir $HOST_OWNER -> $REDIS_UID 0333 -> 0600
+ appendonlydir/foo.aof dir $HOST_OWNER -> $REDIS_UID 0333 -> 0600
+ dump.rdb file $HOST_OWNER -> $REDIS_UID 0333 -> 0600
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /data)
+ run_docker_and_test_ownership_with_common_flags_for_server
+}
+
+test_data_dir_owner_and_perms_changed_by_server_when_data_is_empty_and_RO() {
+ dir_structure="
+ . dir $HOST_OWNER -> $REDIS_UID 0555 -> 0700
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /data)
+ run_docker_and_test_ownership_with_common_flags_for_server
+}
+
+test_data_dir_owner_and_perms_not_changed_by_server_when_data_is_RW() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0777 -> 0777
+ appendonlydir dir $HOST_OWNER -> $HOST_UID 0666 -> 0666
+ dump.rdb file $HOST_OWNER -> $HOST_UID 0666 -> 0666
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /data)
+ run_docker_and_test_ownership_with_common_flags_for_server
+}
+
+test_data_dir_owner_and_perms_not_changed_by_server_when_data_contains_unknown_file() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0555 -> 0555
+ appendonlydir dir $HOST_OWNER -> $HOST_UID 0444 -> 0444
+ dump.rdb file $HOST_OWNER -> $HOST_UID 0444 -> 0444
+ garbage.file file $HOST_OWNER -> $HOST_UID 0444 -> 0444
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /data)
+ run_docker_and_test_ownership_with_common_flags_for_server
+}
+
+test_data_dir_owner_and_perms_not_changed_by_server_when_data_contains_unknown_subdir() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0555 -> 0555
+ somedir dir $HOST_OWNER -> $HOST_UID 0444 -> 0444
+ dump.rdb file $HOST_OWNER -> $HOST_UID 0444 -> 0444
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /data)
+ run_docker_and_test_ownership_with_common_flags_for_server
+}
+
+test_data_dir_owner_not_changed_when_sentinel() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0555 -> 0555
+ appendonlydir dir $HOST_OWNER -> $HOST_UID 0333 -> 0333
+ dump.rdb file $HOST_OWNER -> $HOST_UID 0333 -> 0333
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /data)
+ run_docker_and_test_ownership_with_common_flags_for_sentinel
+}
+
+
+test_config_owner_not_changed_by_server_when_config_is_readable() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0555 -> 0555
+ redis.conf file $HOST_OWNER -> $HOST_UID 0444 -> 0444
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /etc/redis)
+ run_docker_and_test_ownership_with_common_flags_for_server /etc/redis/redis.conf
+}
+
+test_only_config_file_owner_and_perms_changed_by_server_when_only_config_is_not_readable() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0555 -> 0555
+ redis.conf file $HOST_OWNER -> $REDIS_UID 0000 -> 0400
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /etc/redis)
+ run_docker_and_test_ownership_with_common_flags_for_server /etc/redis/redis.conf
+}
+
+test_config_file_and_dir_owner_and_perms_changed_by_server_when_not_readable() {
+ dir_structure="
+ . dir $HOST_OWNER -> $REDIS_UID 0000 -> 0400
+ redis.conf file $HOST_OWNER -> $REDIS_UID 0000 -> 0400
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /etc/redis)
+ run_docker_and_test_ownership_with_common_flags_for_server /etc/redis/redis.conf
+}
+
+test_config_owner_and_perms_not_changed_when_unknown_file_exists() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0000 -> 0000
+ redis.conf file $HOST_OWNER -> $HOST_UID 0000 -> 0000
+ garbage.file file $HOST_OWNER -> $HOST_UID 0000 -> 0000
+ "
+
+ common_flags=(--dir-structure "$dir_structure" --mount-target /etc/redis)
+ run_docker_and_test_ownership_with_common_flags_for_server /etc/redis/redis.conf
+ run_docker_and_test_ownership_with_common_flags_for_sentinel /etc/redis/redis.conf
+}
+
+test_config_owner_and_perms_not_changed_when_unknown_subdir_exists() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0000 -> 0000
+ redis.conf file $HOST_OWNER -> $HOST_UID 0000 -> 0000
+ some dir $HOST_OWNER -> $HOST_UID 0000 -> 0000
+ "
+
+ common_flags=(--dir-structure "$dir_structure" --mount-target /etc/redis)
+ run_docker_and_test_ownership_with_common_flags_for_server /etc/redis/redis.conf
+ run_docker_and_test_ownership_with_common_flags_for_sentinel /etc/redis/redis.conf
+}
+
+test_config_owner_and_perms_not_changed_by_sentinel_when_config_is_RW() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0777 -> 0777
+ sentinel.conf file $HOST_OWNER -> $HOST_UID 0666 -> 0666
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /etc/redis/sentinel)
+ run_docker_and_test_ownership_with_common_flags_for_sentinel /etc/redis/sentinel/sentinel.conf
+}
+
+test_config_file_and_dir_owner_and_perms_changed_by_sentinel_when_RO() {
+ dir_structure="
+ . dir $HOST_OWNER -> $REDIS_UID 0555 -> 0700
+ sentinel.conf file $HOST_OWNER -> $REDIS_UID 0400 -> 0600
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /etc/redis/sentinel)
+ run_docker_and_test_ownership_with_common_flags_for_sentinel /etc/redis/sentinel/sentinel.conf
+}
+
+test_config_dir_owner_and_perms_changed_by_sentinel_when_only_dir_is_RO() {
+ dir_structure="
+ . dir $HOST_OWNER -> $REDIS_UID 0555 -> 0700
+ sentinel.conf file $HOST_OWNER -> $HOST_UID 0666 -> 0666
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /etc/redis/sentinel)
+ run_docker_and_test_ownership_with_common_flags_for_sentinel /etc/redis/sentinel/sentinel.conf
+}
+
+test_config_owner_and_perms_changed_by_sentinel_when_config_is_WO() {
+ dir_structure="
+ . dir $HOST_OWNER -> $REDIS_UID 0333 -> 0700
+ sentinel.conf file $HOST_OWNER -> $REDIS_UID 0222 -> 0600
+ "
+ common_flags=(--dir-structure "$dir_structure" --mount-target /etc/redis/sentinel)
+ run_docker_and_test_ownership_with_common_flags_for_sentinel /etc/redis/sentinel/sentinel.conf
+}
+
+# test that entrypoint tries to start redis even when config is non existent dir
+test_redis_start_reached_when_config_dir_does_not_exist() {
+ assert_has_config_error() {
+ # shellcheck disable=SC2317
+ assertContains "$docker_output" "Fatal error, can't open config file"
+ # shellcheck disable=SC2317
+ assertContains "$docker_output" "No such file or directory"
+ }
+ common_flags=(--mount-target /etc/somewhere --extra-assert assert_has_config_error)
+ run_docker_and_test_ownership_with_common_flags_for_server /etc/nowhere/redis.conf
+}
+
+test_redis_start_reached_when_chown_on_data_dir_is_denied() {
+ assert_internal() {
+ # shellcheck disable=SC2317
+ assert_redis_v8 "$docker_output"
+ }
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0333 -> 0700
+ dump.rdb file root:root -> 0 0222 -> 0222
+ "
+ common_flags=(--mount-target /data
+ --dir-structure "$dir_structure"
+ --extra-assert assert_internal
+ --docker-flags "--cap-drop=chown"
+ )
+ run_docker_and_test_ownership_with_common_flags_for_server
+}
+
+test_data_dir_owner_and_perms_not_changed_by_server_when_data_is_RO_and_SKIP_FIX_PERMS_is_used() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0555 -> 0555
+ datum.rdb file $HOST_OWNER -> $HOST_UID 0444 -> 0444
+ "
+ common_flags=(--mount-target /data
+ --dir-structure "$dir_structure"
+ --docker-flags "-e SKIP_FIX_PERMS=1"
+ )
+ run_docker_and_test_ownership_with_common_flags_for_server
+}
+
+test_config_owner_and_perms_not_changed_by_sentinel_when_config_is_RO_and_SKIP_FIX_PERMS_is_used() {
+ dir_structure="
+ . dir $HOST_OWNER -> $HOST_UID 0555 -> 0555
+ sentinel.conf file $HOST_OWNER -> $HOST_UID 0444 -> 0444
+ "
+ common_flags=(--mount-target /etc/redis/sentinel
+ --dir-structure "$dir_structure"
+ --docker-flags "-e SKIP_FIX_PERMS=1"
+ )
+ run_docker_and_test_ownership_with_common_flags_for_sentinel /etc/redis/sentinel/sentinel.conf
+}
+
+
+
+test_redis_server_persistence_with_bind_mount() {
+ dir=$(mktemp -d -p .)
+
+ # make data directory non writable
+ chmod 0444 "$dir"
+
+ container=$(docker run --rm -d -v "$(pwd)/$dir":/data "$REDIS_IMG" --appendonly yes)
+
+ result=$(echo save | docker exec -i "$container" redis-cli)
+ assertEquals "OK" "$result"
+
+ # save container hash as a value
+ result=$(echo "SET FOO $container" | docker exec -i "$container" redis-cli)
+ assertEquals "OK" "$result"
+
+ docker stop "$container" >/dev/null
+
+ # change the owner
+ sudo chown -R "$HOST_OWNER" "$dir"
+
+ container2=$(docker run --rm -d -v "$(pwd)/$dir":/data "$REDIS_IMG")
+ value=$(echo "GET FOO" | docker exec -i "$container2" redis-cli)
+ assertEquals "$container" "$value"
+
+ docker stop "$container2" >/dev/null
+
+ sudo rm -rf "$dir"
+}
+
+test_redis_server_persistence_with_volume() {
+ docker volume rm test_redis >/dev/null 2>&1 || :
+
+ docker volume create test_redis >/dev/null
+
+ # change owner of the data volume
+ docker run --rm -v test_redis:/data --entrypoint=/bin/sh "$REDIS_IMG" -c 'chown -R 0:0 /data'
+
+ container=$(docker run --rm -d -v test_redis:/data "$REDIS_IMG" --appendonly yes)
+
+ result=$(echo save | docker exec -i "$container" redis-cli)
+ assertEquals "OK" "$result"
+
+ # save container hash as a value
+ result=$(echo "SET FOO $container" | docker exec -i "$container" redis-cli)
+ assertEquals "OK" "$result"
+
+ docker stop "$container" >/dev/null
+
+ # change owner and permissions of files in data volume
+ docker run --rm -v test_redis:/data --entrypoint=/bin/sh "$REDIS_IMG" -c 'chown -R 0:0 /data && chmod 0000 -R /data'
+
+ container2=$(docker run --rm -d -v test_redis:/data "$REDIS_IMG")
+ value=$(echo "GET FOO" | docker exec -i "$container2" redis-cli)
+ assertEquals "$container" "$value"
+
+ docker stop "$container2" >/dev/null
+
+ docker volume rm test_redis >/dev/null || :
+}
+
+test_redis_process_uid_and_gid_are_redis() {
+ run_redis_docker_and_check_uid_gid ""
+ run_redis_docker_and_check_uid_gid redis-server
+ run_redis_docker_and_check_uid_gid /usr/local/bin/redis-server
+
+ run_redis_docker_and_check_uid_gid --expected-cmd redis-sentinel redis-sentinel /etc/sentinel/sentinel.conf
+ run_redis_docker_and_check_uid_gid --expected-cmd redis-sentinel /usr/local/bin/redis-sentinel /etc/sentinel/sentinel.conf
+ run_redis_docker_and_check_uid_gid --expected-cmd "[sentinel]" /etc/sentinel/sentinel.conf --sentinel
+ run_redis_docker_and_check_uid_gid --expected-cmd "[sentinel]" redis-server /etc/sentinel/sentinel.conf --sentinel
+ run_redis_docker_and_check_uid_gid --expected-cmd "[sentinel]" /usr/local/bin/redis-server /etc/sentinel/sentinel.conf --sentinel
+}
+
+test_redis_process_uid_and_gid_respects_docker_user_arg() {
+ read -r daemon_user_uid _ <<< "$(get_container_user_uid_gid_on_the_host daemon:daemon)"
+
+ # disable persistence as directory data dir would not be writable
+ common_flags=(--user daemon --group daemon --docker-flags "--user daemon")
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" "" --save ""
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" redis-server --save ""
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" /usr/local/bin/redis-server --save ""
+
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" --file-owner "$daemon_user_uid" --expected-cmd redis-sentinel redis-sentinel /etc/sentinel/sentinel.conf
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" --file-owner "$daemon_user_uid" --expected-cmd redis-sentinel /usr/local/bin/redis-sentinel /etc/sentinel/sentinel.conf
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" --file-owner "$daemon_user_uid" --expected-cmd "[sentinel]" /etc/sentinel/sentinel.conf --sentinel
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" --file-owner "$daemon_user_uid" --expected-cmd "[sentinel]" redis-server /etc/sentinel/sentinel.conf --sentinel
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" --file-owner "$daemon_user_uid" --expected-cmd "[sentinel]" /usr/local/bin/redis-server /etc/sentinel/sentinel.conf --sentinel
+}
+
+test_redis_process_uid_and_gid_are_root_when_SKIP_DROP_PRIVS_is_used() {
+ common_flags=(--user root --group root --docker-flags "-e SKIP_DROP_PRIVS=1")
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" "" --save ""
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" redis-server --save ""
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" /usr/local/bin/redis-server --save ""
+
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" --expected-cmd redis-sentinel redis-sentinel /etc/sentinel/sentinel.conf
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" --expected-cmd redis-sentinel /usr/local/bin/redis-sentinel /etc/sentinel/sentinel.conf
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" --expected-cmd "[sentinel]" /etc/sentinel/sentinel.conf --sentinel
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" --expected-cmd "[sentinel]" redis-server /etc/sentinel/sentinel.conf --sentinel
+ run_redis_docker_and_check_uid_gid "${common_flags[@]}" --expected-cmd "[sentinel]" /usr/local/bin/redis-server /etc/sentinel/sentinel.conf --sentinel
+}
+
+test_redis_server_modules_are_loaded() {
+ run_redis_docker_and_check_modules
+ run_redis_docker_and_check_modules redis-server
+ run_redis_docker_and_check_modules /usr/local/bin/redis-server
+}
+
+# shellcheck disable=SC1091
+. ./shunit2
diff --git a/test/shunit2 b/test/shunit2
new file mode 100755
index 000000000..7b7c7c199
--- /dev/null
+++ b/test/shunit2
@@ -0,0 +1,1612 @@
+#! /bin/sh
+# vim:et:ft=sh:sts=2:sw=2
+#
+# shUnit2 -- Unit testing framework for Unix shell scripts.
+#
+# Copyright 2008-2021 Kate Ward. All Rights Reserved.
+# Released under the Apache 2.0 license.
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Author: kate.ward@forestent.com (Kate Ward)
+# https://github.com/kward/shunit2
+#
+# shUnit2 is a xUnit based unit test framework for Bourne shell scripts. It is
+# based on the popular JUnit unit testing framework for Java.
+#
+# `expr` may be antiquated, but it is the only solution in some cases.
+# shellcheck disable=SC2003
+# Allow usage of legacy backticked `...` notation instead of $(...).
+# shellcheck disable=SC2006
+
+# Return if shunit2 already loaded.
+if test -n "${SHUNIT_VERSION:-}"; then
+ exit 0
+fi
+SHUNIT_VERSION='2.1.9pre'
+
+# Return values that scripts can use.
+SHUNIT_TRUE=0
+SHUNIT_FALSE=1
+SHUNIT_ERROR=2
+
+# Determine if `builtin` command exists.
+__SHUNIT_BUILTIN='builtin'
+# shellcheck disable=2039
+if ! ("${__SHUNIT_BUILTIN}" echo 123 >/dev/null 2>&1); then
+ __SHUNIT_BUILTIN=''
+fi
+
+# Determine some reasonable command defaults.
+__SHUNIT_CMD_ECHO_ESC='echo -e'
+# shellcheck disable=SC2039,SC3037
+if ${__SHUNIT_BUILTIN} [ "`echo -e test`" = '-e test' ]; then
+ __SHUNIT_CMD_ECHO_ESC='echo'
+fi
+
+# Determine if `date` supports nanosecond resolution.
+__SHUNIT_CMD_DATE_SECONDS='date +%s.%N'
+if ${__SHUNIT_BUILTIN} [ "`date +%N`" = '%N' ]; then
+ __SHUNIT_CMD_DATE_SECONDS='date +%s'
+fi
+
+# Determine `bc` command.
+__SHUNIT_CMD_BC='bc'
+if ! (${__SHUNIT_CMD_BC} --help >/dev/null 2>&1); then
+ __SHUNIT_CMD_BC='busybox bc'
+fi
+if ! (${__SHUNIT_CMD_BC} --help >/dev/null 2>&1); then
+ __SHUNIT_CMD_BC=''
+fi
+
+# Determine `dc` command.
+__SHUNIT_CMD_DC='dc'
+if ! (${__SHUNIT_CMD_DC} --help >/dev/null 2>&1); then
+ __SHUNIT_CMD_DC='busybox dc'
+fi
+if ! (${__SHUNIT_CMD_DC} --help >/dev/null 2>&1); then
+ __SHUNIT_CMD_DC=''
+fi
+
+# Format float numbers to the single style from different tools.
+# Args:
+# num: string: float number to format
+# Returns:
+# string: formatted number. Empty string if error occurs.
+_shunit_float_format() {
+ # Double-dot number is an error.
+ # No need to format if the number is an integer.
+ case "${1}" in
+ *.*.*)
+ return
+ ;;
+ *.*)
+ ;;
+ *)
+ echo "${1}"
+ return
+ ;;
+ esac
+
+ _shunit_format_result_="$1"
+
+ # Add leading zero if needed.
+ _shunit_format_result_="$(echo "${_shunit_format_result_}" \
+ |command sed 's/^\./0./g')"
+
+ # Remove trailing zeros.
+ _shunit_format_result_="$(echo "${_shunit_format_result_}" \
+ |command sed 's/0\+$//g')"
+
+ # Remove trailing dot.
+ _shunit_format_result_="$(echo "${_shunit_format_result_}" \
+ |command sed 's/\.$//g')"
+
+ # Print the result.
+ echo "${_shunit_format_result_}"
+ unset _shunit_format_result_
+}
+
+# Calculate numbers using bc.
+# Args:
+# left: string: left operand (may be float point)
+# operation: string: operation (+ - * /)
+# right: string: right operand (may be float point)
+# Returns:
+# string: result
+_shunit_calc_bc() {
+ _shunit_output_="$(echo "$@" \
+ |command ${__SHUNIT_CMD_BC:?})"
+ shunit_return=$?
+ if ${__SHUNIT_BUILTIN} [ ${shunit_return} -eq ${SHUNIT_TRUE} ]; then
+ _shunit_float_format "${_shunit_output_}"
+ shunit_return=$?
+ fi
+
+ unset _shunit_output_
+ return ${shunit_return}
+}
+
+# Calculate numbers using dc.
+# Args:
+# left: string: left operand (may be float point)
+# operation: string: operation (+ - * /)
+# right: string: right operand (may be float point)
+# Returns:
+# string: result
+_shunit_calc_dc() {
+ _shunit_output_="$(echo "$1" "$3" "$2" "p" \
+ |command ${__SHUNIT_CMD_DC:?})"
+ shunit_return=$?
+ if ${__SHUNIT_BUILTIN} [ ${shunit_return} -eq ${SHUNIT_TRUE} ]; then
+ _shunit_float_format "${_shunit_output_}"
+ shunit_return=$?
+ fi
+
+ unset _shunit_output_
+ return ${shunit_return}
+}
+
+# Calculate numbers using expr.
+# Args:
+# left: string: left integer number operand
+# operation: string: operation (+ - * /)
+# right: string: right integer number operand
+# Returns:
+# string: result. Empty string if error occurs.
+_shunit_calc_expr() {
+ expr "$@" 2>/dev/null || ${__SHUNIT_BUILTIN} true
+}
+
+# Determine what command to use for calculating numbers.
+__SHUNIT_CMD_CALC='_shunit_calc_bc'
+if ! ("${__SHUNIT_CMD_CALC}" 1 + 2 >/dev/null 2>&1); then
+ __SHUNIT_CMD_CALC=_shunit_calc_dc
+fi
+if ! ("${__SHUNIT_CMD_CALC}" 1 + 2 >/dev/null 2>&1); then
+ __SHUNIT_CMD_CALC=_shunit_calc_expr
+fi
+
+# Commands a user can override if needed.
+__SHUNIT_CMD_TPUT='tput'
+SHUNIT_CMD_TPUT=${SHUNIT_CMD_TPUT:-${__SHUNIT_CMD_TPUT}}
+
+# Enable color output. Options are 'auto', 'always', or 'never'.
+SHUNIT_COLOR=${SHUNIT_COLOR:-auto}
+
+#
+# Internal constants.
+#
+
+__SHUNIT_MODE_SOURCED='sourced'
+__SHUNIT_MODE_STANDALONE='standalone'
+__SHUNIT_PARENT=${SHUNIT_PARENT:-$0}
+
+# User provided test prefix to display in front of the name of the test being
+# executed. Define by setting the SHUNIT_TEST_PREFIX variable.
+__SHUNIT_TEST_PREFIX=${SHUNIT_TEST_PREFIX:-}
+
+# ANSI colors.
+__SHUNIT_ANSI_NONE='\033[0m'
+__SHUNIT_ANSI_RED='\033[1;31m'
+__SHUNIT_ANSI_GREEN='\033[1;32m'
+__SHUNIT_ANSI_YELLOW='\033[1;33m'
+__SHUNIT_ANSI_CYAN='\033[1;36m'
+
+#
+# Internal variables.
+#
+
+# Variables.
+__shunit_lineno='' # Line number of executed test.
+__shunit_mode=${__SHUNIT_MODE_SOURCED} # Operating mode.
+__shunit_reportGenerated=${SHUNIT_FALSE} # Is report generated.
+__shunit_script='' # Filename of unittest script (standalone mode).
+__shunit_skip=${SHUNIT_FALSE} # Is skipping enabled.
+__shunit_suite='' # Suite of tests to execute.
+__shunit_clean=${SHUNIT_FALSE} # _shunit_cleanup() was already called.
+__shunit_suiteName='' # Text name of current test suite.
+__shunit_xmlSuiteName='' # XML-ready text name of current test suite.
+
+# JUnit XML variables.
+__shunit_junitXmlOutputFile='' # File to use for JUnit XML output in addition to stdout.
+__shunit_junitXmlTestCases='' # Test cases info in the JUnit XML format for output
+__shunit_junitXmlCurrentTestCaseErrors='' # Current test case error info in the JUnit XML format for output
+
+# Time variables
+__shunit_startSuiteTime='' # When the suite execution was started
+__shunit_endSuiteTime='' # When the suite execution ended
+__shunit_startCaseTime='' # When the case execution was started
+__shunit_endCaseTime='' # When the case execution ended
+
+# ANSI colors (populated by _shunit_configureColor()).
+__shunit_ansi_none=''
+__shunit_ansi_red=''
+__shunit_ansi_green=''
+__shunit_ansi_yellow=''
+__shunit_ansi_cyan=''
+
+# Counts of tests.
+__shunit_testSuccess=${SHUNIT_TRUE}
+__shunit_testsTotal=0
+__shunit_testsPassed=0
+__shunit_testsFailed=0
+
+# Counts of asserts.
+__shunit_assertsTotal=0
+__shunit_assertsPassed=0
+__shunit_assertsFailed=0
+__shunit_assertsSkipped=0
+__shunit_assertsCurrentTest=0
+
+#
+# Internal functions.
+#
+
+# Logging.
+_shunit_warn() {
+ ${__SHUNIT_CMD_ECHO_ESC} "${__shunit_ansi_yellow}shunit2:WARN${__shunit_ansi_none} $*" >&2
+}
+_shunit_error() {
+ ${__SHUNIT_CMD_ECHO_ESC} "${__shunit_ansi_red}shunit2:ERROR${__shunit_ansi_none} $*" >&2
+}
+_shunit_fatal() {
+ ${__SHUNIT_CMD_ECHO_ESC} "${__shunit_ansi_red}shunit2:FATAL${__shunit_ansi_none} $*" >&2
+ exit ${SHUNIT_ERROR}
+}
+
+#
+# Macros.
+#
+
+# shellcheck disable=SC2016,SC2089
+_SHUNIT_LINENO_='eval __shunit_lineno=""; if ${__SHUNIT_BUILTIN} [ "${1:-}" = "--lineno" ] && ${__SHUNIT_BUILTIN} [ -n "${2:-}" ]; then __shunit_lineno="[${2}]"; shift 2; fi;'
+
+#
+# Setup.
+#
+
+# Specific shell checks.
+if ${__SHUNIT_BUILTIN} [ -n "${ZSH_VERSION:-}" ]; then
+ setopt |grep "^shwordsplit$" >/dev/null
+ if ${__SHUNIT_BUILTIN} [ $? -ne ${SHUNIT_TRUE} ]; then
+ _shunit_fatal 'zsh shwordsplit option is required for proper operation'
+ fi
+ if ${__SHUNIT_BUILTIN} [ -z "${SHUNIT_PARENT:-}" ]; then
+ _shunit_fatal "zsh does not pass \$0 through properly. please declare \
+\"SHUNIT_PARENT=\$0\" before calling shUnit2"
+ fi
+fi
+
+# Set the constants readonly.
+__shunit_constants=`set |grep '^__SHUNIT_' |cut -d= -f1`
+echo "${__shunit_constants}" |grep '^Binary file' >/dev/null && \
+ __shunit_constants=`set |grep -a '^__SHUNIT_' |cut -d= -f1`
+for __shunit_const in ${__shunit_constants}; do
+ if ${__SHUNIT_BUILTIN} [ -z "${ZSH_VERSION:-}" ]; then
+ readonly "${__shunit_const}"
+ else
+ case ${ZSH_VERSION} in
+ [123].*) readonly "${__shunit_const}" ;;
+ *)
+ # Declare readonly constants globally.
+ # shellcheck disable=SC2039,SC3045
+ readonly -g "${__shunit_const}"
+ esac
+ fi
+done
+unset __shunit_const __shunit_constants
+
+#-----------------------------------------------------------------------------
+# Assertion functions.
+#
+
+# Assert that two values are equal to one another.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertEquals() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertEquals() requires two or three arguments; $# given"
+ _shunit_assertFail
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_expected_=$1
+ shunit_actual_=$2
+
+ shunit_return=${SHUNIT_TRUE}
+ if ${__SHUNIT_BUILTIN} [ "${shunit_expected_}" = "${shunit_actual_}" ]; then
+ _shunit_assertPass
+ else
+ failNotEquals "${shunit_message_}" "${shunit_expected_}" "${shunit_actual_}"
+ shunit_return=${SHUNIT_FALSE}
+ fi
+
+ unset shunit_message_ shunit_expected_ shunit_actual_
+ return ${shunit_return}
+}
+# shellcheck disable=SC2016,SC2034
+_ASSERT_EQUALS_='eval assertEquals --lineno "${LINENO:-}"'
+
+# Assert that two values are not equal to one another.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNotEquals() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertNotEquals() requires two or three arguments; $# given"
+ _shunit_assertFail
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_expected_=$1
+ shunit_actual_=$2
+
+ shunit_return=${SHUNIT_TRUE}
+ if ${__SHUNIT_BUILTIN} [ "${shunit_expected_}" != "${shunit_actual_}" ]; then
+ _shunit_assertPass
+ else
+ failSame "${shunit_message_}" "${shunit_expected_}" "${shunit_actual_}"
+ shunit_return=${SHUNIT_FALSE}
+ fi
+
+ unset shunit_message_ shunit_expected_ shunit_actual_
+ return ${shunit_return}
+}
+# shellcheck disable=SC2016,SC2034
+_ASSERT_NOT_EQUALS_='eval assertNotEquals --lineno "${LINENO:-}"'
+
+# Assert that a container contains a content.
+#
+# Args:
+# message: string: failure message [optional]
+# container: string: container to analyze
+# content: string: content to find
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertContains() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertContains() requires two or three arguments; $# given"
+ _shunit_assertFail
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_container_=$1
+ shunit_content_=$2
+ shunit_return=${SHUNIT_TRUE}
+ if echo "${shunit_container_}" |grep -F -- "${shunit_content_}" >/dev/null; then
+ _shunit_assertPass
+ else
+ failNotFound "${shunit_message_}" "${shunit_content_}"
+ shunit_return=${SHUNIT_FALSE}
+ fi
+
+ unset shunit_message_ shunit_container_ shunit_content_
+ return ${shunit_return}
+}
+# shellcheck disable=SC2016,SC2034
+_ASSERT_CONTAINS_='eval assertContains --lineno "${LINENO:-}"'
+
+# Assert that a container does not contain a content.
+#
+# Args:
+# message: string: failure message [optional]
+# container: string: container to analyze
+# content: string: content to look for
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNotContains() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertNotContains() requires two or three arguments; $# given"
+ _shunit_assertFail
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_container_=$1
+ shunit_content_=$2
+
+ shunit_return=${SHUNIT_TRUE}
+ if echo "$shunit_container_" |grep -F -- "$shunit_content_" > /dev/null; then
+ failFound "${shunit_message_}" "${shunit_content_}"
+ shunit_return=${SHUNIT_FALSE}
+ else
+ _shunit_assertPass
+ fi
+
+ unset shunit_message_ shunit_container_ shunit_content_
+ return ${shunit_return}
+}
+# shellcheck disable=SC2016,SC2034
+_ASSERT_NOT_CONTAINS_='eval assertNotContains --lineno "${LINENO:-}"'
+
+# Assert that a value is null (i.e. an empty string).
+#
+# Args:
+# message: string: failure message [optional]
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNull() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -gt 2 ]; then
+ # Allowing 0 arguments as $1 might actually be null.
+ _shunit_error "assertNull() requires one or two arguments; $# given"
+ _shunit_assertFail
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+
+ ${__SHUNIT_BUILTIN} test -z "${1:-}"
+ assertTrue "${shunit_message_}" $?
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+# shellcheck disable=SC2016,SC2034
+_ASSERT_NULL_='eval assertNull --lineno "${LINENO:-}"'
+
+# Assert that a value is not null (i.e. a non-empty string).
+#
+# Args:
+# message: string: failure message [optional]
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNotNull() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -gt 2 ]; then
+ # Allowing 0 arguments as $1 might actually be null.
+ _shunit_error "assertNotNull() requires one or two arguments; $# given"
+ _shunit_assertFail
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+
+ ${__SHUNIT_BUILTIN} test -n "${1:-}"
+ assertTrue "${shunit_message_}" $?
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+# shellcheck disable=SC2016,SC2034
+_ASSERT_NOT_NULL_='eval assertNotNull --lineno "${LINENO:-}"'
+
+# Assert that two values are the same (i.e. equal to one another).
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertSame() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertSame() requires two or three arguments; $# given"
+ _shunit_assertFail
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ assertEquals "${shunit_message_}" "$1" "$2"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+# shellcheck disable=SC2016,SC2034
+_ASSERT_SAME_='eval assertSame --lineno "${LINENO:-}"'
+
+# Assert that two values are not the same (i.e. not equal to one another).
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertNotSame() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "assertNotSame() requires two or three arguments; $# given"
+ _shunit_assertFail
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_:-}$1"
+ shift
+ fi
+ assertNotEquals "${shunit_message_}" "$1" "$2"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+# shellcheck disable=SC2016,SC2034
+_ASSERT_NOT_SAME_='eval assertNotSame --lineno "${LINENO:-}"'
+
+# Assert that a value or shell test condition is true.
+#
+# In shell, a value of 0 is true and a non-zero value is false. Any integer
+# value passed can thereby be tested.
+#
+# Shell supports much more complicated tests though, and a means to support
+# them was needed. As such, this function tests that conditions are true or
+# false through evaluation rather than just looking for a true or false.
+#
+# The following test will succeed:
+# assertTrue 0
+# assertTrue "[ 34 -gt 23 ]"
+# The following test will fail with a message:
+# assertTrue 123
+# assertTrue "test failed" "[ -r '/non/existent/file' ]"
+#
+# Args:
+# message: string: failure message [optional]
+# condition: string: integer value or shell conditional statement
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertTrue() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 1 -o $# -gt 2 ]; then
+ _shunit_error "assertTrue() takes one or two arguments; $# given"
+ _shunit_assertFail
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_condition_=$1
+
+ # See if condition is an integer, i.e. a return value.
+ shunit_return=${SHUNIT_TRUE}
+ if ${__SHUNIT_BUILTIN} [ -z "${shunit_condition_}" ]; then
+ # Null condition.
+ shunit_return=${SHUNIT_FALSE}
+ elif (expr \( "${shunit_condition_}" + '0' \) '=' "${shunit_condition_}" >/dev/null 2>&1)
+ then
+ # Possible return value. Treating 0 as true, and non-zero as false.
+ if ${__SHUNIT_BUILTIN} [ "${shunit_condition_}" -ne 0 ]; then
+ shunit_return=${SHUNIT_FALSE}
+ fi
+ else
+ # Hopefully... a condition.
+ if ! eval "${shunit_condition_}" >/dev/null 2>&1; then
+ shunit_return=${SHUNIT_FALSE}
+ fi
+ fi
+
+ # Record the test.
+ if ${__SHUNIT_BUILTIN} [ ${shunit_return} -eq ${SHUNIT_TRUE} ]; then
+ _shunit_assertPass
+ else
+ _shunit_assertFail "${shunit_message_}"
+ fi
+
+ unset shunit_message_ shunit_condition_
+ return ${shunit_return}
+}
+# shellcheck disable=SC2016,SC2034
+_ASSERT_TRUE_='eval assertTrue --lineno "${LINENO:-}"'
+
+# Assert that a value or shell test condition is false.
+#
+# In shell, a value of 0 is true and a non-zero value is false. Any integer
+# value passed can thereby be tested.
+#
+# Shell supports much more complicated tests though, and a means to support
+# them was needed. As such, this function tests that conditions are true or
+# false through evaluation rather than just looking for a true or false.
+#
+# The following test will succeed:
+# assertFalse 1
+# assertFalse "[ 'apples' = 'oranges' ]"
+# The following test will fail with a message:
+# assertFalse 0
+# assertFalse "test failed" "[ 1 -eq 1 -a 2 -eq 2 ]"
+#
+# Args:
+# message: string: failure message [optional]
+# condition: string: integer value or shell conditional statement
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+assertFalse() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 1 -o $# -gt 2 ]; then
+ _shunit_error "assertFalse() requires one or two arguments; $# given"
+ _shunit_assertFail
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_condition_=$1
+
+ # See if condition is an integer, i.e. a return value.
+ shunit_return=${SHUNIT_TRUE}
+ if ${__SHUNIT_BUILTIN} [ -z "${shunit_condition_}" ]; then
+ # Null condition.
+ shunit_return=${SHUNIT_TRUE}
+ elif (expr \( "${shunit_condition_}" + '0' \) '=' "${shunit_condition_}" >/dev/null 2>&1); then
+ # Possible return value. Treating 0 as true, and non-zero as false.
+ if ${__SHUNIT_BUILTIN} [ "${shunit_condition_}" -eq 0 ]; then
+ shunit_return=${SHUNIT_FALSE}
+ fi
+ else
+ # Hopefully... a condition.
+ # shellcheck disable=SC2086
+ if eval ${shunit_condition_} >/dev/null 2>&1; then
+ shunit_return=${SHUNIT_FALSE}
+ fi
+ fi
+
+ # Record the test.
+ if ${__SHUNIT_BUILTIN} [ "${shunit_return}" -eq "${SHUNIT_TRUE}" ]; then
+ _shunit_assertPass
+ else
+ _shunit_assertFail "${shunit_message_}"
+ fi
+
+ unset shunit_message_ shunit_condition_
+ return "${shunit_return}"
+}
+# shellcheck disable=SC2016,SC2034
+_ASSERT_FALSE_='eval assertFalse --lineno "${LINENO:-}"'
+
+#-----------------------------------------------------------------------------
+# Failure functions.
+#
+
+# Records a test failure.
+#
+# Args:
+# message: string: failure message [optional]
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+fail() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -gt 1 ]; then
+ _shunit_error "fail() requires zero or one arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 1 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+
+ _shunit_assertFail "${shunit_message_}"
+
+ unset shunit_message_
+ return ${SHUNIT_FALSE}
+}
+# shellcheck disable=SC2016,SC2034
+_FAIL_='eval fail --lineno "${LINENO:-}"'
+
+# Records a test failure, stating two values were not equal.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failNotEquals() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "failNotEquals() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_expected_=$1
+ shunit_actual_=$2
+
+ shunit_message_=${shunit_message_%% }
+ _shunit_assertFail "${shunit_message_:+${shunit_message_} }expected:<${shunit_expected_}> but was:<${shunit_actual_}>"
+
+ unset shunit_message_ shunit_expected_ shunit_actual_
+ return ${SHUNIT_FALSE}
+}
+# shellcheck disable=SC2016,SC2034
+_FAIL_NOT_EQUALS_='eval failNotEquals --lineno "${LINENO:-}"'
+
+# Records a test failure, stating a value was found.
+#
+# Args:
+# message: string: failure message [optional]
+# content: string: found value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failFound() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 1 -o $# -gt 2 ]; then
+ _shunit_error "failFound() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_content_=$1
+
+ shunit_message_=${shunit_message_%% }
+ _shunit_assertFail "${shunit_message_:+${shunit_message_} }found:<${shunit_content_}>"
+
+ unset shunit_message_ shunit_content_
+ return ${SHUNIT_FALSE}
+}
+# shellcheck disable=SC2016,SC2034
+_FAIL_FOUND_='eval failFound --lineno "${LINENO:-}"'
+
+# Records a test failure, stating a content was not found.
+#
+# Args:
+# message: string: failure message [optional]
+# content: string: content not found
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failNotFound() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 1 -o $# -gt 2 ]; then
+ _shunit_error "failNotFound() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ shunit_content_=$1
+
+ shunit_message_=${shunit_message_%% }
+ _shunit_assertFail "${shunit_message_:+${shunit_message_} }not found:<${shunit_content_}>"
+
+ unset shunit_message_ shunit_content_
+ return ${SHUNIT_FALSE}
+}
+# shellcheck disable=SC2016,SC2034
+_FAIL_NOT_FOUND_='eval failNotFound --lineno "${LINENO:-}"'
+
+# Records a test failure, stating two values should have been the same.
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failSame() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "failSame() requires two or three arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+
+ shunit_message_=${shunit_message_%% }
+ _shunit_assertFail "${shunit_message_:+${shunit_message_} }expected not same"
+
+ unset shunit_message_
+ return ${SHUNIT_FALSE}
+}
+# shellcheck disable=SC2016,SC2034
+_FAIL_SAME_='eval failSame --lineno "${LINENO:-}"'
+
+# Records a test failure, stating two values were not equal.
+#
+# This is functionally equivalent to calling failNotEquals().
+#
+# Args:
+# message: string: failure message [optional]
+# expected: string: expected value
+# actual: string: actual value
+# Returns:
+# integer: success (TRUE/FALSE/ERROR constant)
+failNotSame() {
+ # shellcheck disable=SC2090
+ ${_SHUNIT_LINENO_}
+ if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then
+ _shunit_error "failNotSame() requires one or two arguments; $# given"
+ return ${SHUNIT_ERROR}
+ fi
+ if _shunit_shouldSkip; then
+ return ${SHUNIT_TRUE}
+ fi
+
+ shunit_message_=${__shunit_lineno}
+ if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then
+ shunit_message_="${shunit_message_}$1"
+ shift
+ fi
+ failNotEquals "${shunit_message_}" "$1" "$2"
+ shunit_return=$?
+
+ unset shunit_message_
+ return ${shunit_return}
+}
+# shellcheck disable=SC2016,SC2034
+_FAIL_NOT_SAME_='eval failNotSame --lineno "${LINENO:-}"'
+
+#-----------------------------------------------------------------------------
+# Skipping functions.
+#
+
+# Force remaining assert and fail functions to be "skipped".
+#
+# This function forces the remaining assert and fail functions to be "skipped",
+# i.e. they will have no effect. Each function skipped will be recorded so that
+# the total of asserts and fails will not be altered.
+#
+# Args:
+# message: string: message to provide to user [optional]
+startSkipping() {
+ if ${__SHUNIT_BUILTIN} [ $# -gt 0 ]; then _shunit_warn "[skipping] $*"; fi
+ __shunit_skip=${SHUNIT_TRUE}
+}
+
+# Resume the normal recording behavior of assert and fail calls.
+#
+# Args:
+# None
+endSkipping() { __shunit_skip=${SHUNIT_FALSE}; }
+
+# Returns the state of assert and fail call skipping.
+#
+# Args:
+# None
+# Returns:
+# boolean: (TRUE/FALSE constant)
+isSkipping() { return ${__shunit_skip}; }
+
+#-----------------------------------------------------------------------------
+# Suite functions.
+#
+
+# Stub. This function should contains all unit test calls to be made.
+#
+# DEPRECATED (as of 2.1.0)
+#
+# This function can be optionally overridden by the user in their test suite.
+#
+# If this function exists, it will be called when shunit2 is sourced. If it
+# does not exist, shunit2 will search the parent script for all functions
+# beginning with the word 'test', and they will be added dynamically to the
+# test suite.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#suite() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+# Adds a function name to the list of tests schedule for execution.
+#
+# This function should only be called from within the suite() function.
+#
+# Args:
+# function: string: name of a function to add to current unit test suite
+suite_addTest() {
+ shunit_func_=${1:-}
+
+ __shunit_suite="${__shunit_suite:+${__shunit_suite} }${shunit_func_}"
+ __shunit_testsTotal=`expr "${__shunit_testsTotal}" + 1`
+
+ unset shunit_func_
+}
+
+# Stub. This function will be called once before any tests are run.
+#
+# Common one-time environment preparation tasks shared by all tests can be
+# defined here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#oneTimeSetUp() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+# Stub. This function will be called once after all tests are finished.
+#
+# Common one-time environment cleanup tasks shared by all tests can be defined
+# here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#oneTimeTearDown() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+# Stub. This function will be called before each test is run.
+#
+# Common environment preparation tasks shared by all tests can be defined here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#setUp() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+# Note: see _shunit_mktempFunc() for actual implementation
+# Stub. This function will be called after each test is run.
+#
+# Common environment cleanup tasks shared by all tests can be defined here.
+#
+# This function should be overridden by the user in their unit test suite.
+# Note: see _shunit_mktempFunc() for actual implementation
+#
+# Args:
+# None
+#tearDown() { :; } # DO NOT UNCOMMENT THIS FUNCTION
+
+#------------------------------------------------------------------------------
+# Internal shUnit2 functions.
+#
+
+# Create a temporary directory to store various run-time files in.
+#
+# This function is a cross-platform temporary directory creation tool. Not all
+# OSes have the `mktemp` function, so one is included here.
+#
+# Args:
+# None
+# Outputs:
+# string: the temporary directory that was created
+_shunit_mktempDir() {
+ # Try the standard `mktemp` function.
+ if ( exec mktemp -dqt shunit.XXXXXX 2>/dev/null ); then
+ return
+ fi
+
+ # The standard `mktemp` didn't work. Use our own.
+ # shellcheck disable=SC2039,SC3028
+ if ${__SHUNIT_BUILTIN} [ -r '/dev/urandom' -a -x '/usr/bin/od' ]; then
+ _shunit_random_=`/usr/bin/od -vAn -N4 -tx4 "${_shunit_file_}"
+#! /bin/sh
+exit ${SHUNIT_TRUE}
+EOF
+ command chmod +x "${_shunit_file_}"
+ done
+
+ unset _shunit_file_
+}
+
+# Final cleanup function to leave things as we found them.
+#
+# Besides removing the temporary directory, this function is in charge of the
+# final exit code of the unit test. The exit code is based on how the script
+# was ended (e.g. normal exit, or via Ctrl-C).
+#
+# Args:
+# name: string: name of the trap called (specified when trap defined)
+_shunit_cleanup() {
+ _shunit_name_=$1
+
+ _shunit_signal_=0
+ case "${_shunit_name_}" in
+ EXIT) ;;
+ INT) _shunit_signal_=130 ;; # 2+128
+ TERM) _shunit_signal_=143 ;; # 15+128
+ *)
+ _shunit_error "unrecognized trap value (${_shunit_name_})"
+ ;;
+ esac
+ if ${__SHUNIT_BUILTIN} [ "${_shunit_name_}" != 'EXIT' ]; then
+ _shunit_warn "trapped and now handling the (${_shunit_name_}) signal"
+ fi
+
+ # Do our work.
+ if ${__SHUNIT_BUILTIN} [ ${__shunit_clean} -eq ${SHUNIT_FALSE} ]; then
+ # Ensure tear downs are only called once.
+ __shunit_clean=${SHUNIT_TRUE}
+
+ tearDown || _shunit_warn 'tearDown() returned non-zero return code.'
+ __shunit_endCaseTime=`${__SHUNIT_CMD_DATE_SECONDS}`
+ oneTimeTearDown || \
+ _shunit_warn 'oneTimeTearDown() returned non-zero return code.'
+ __shunit_endSuiteTime=`${__SHUNIT_CMD_DATE_SECONDS}`
+
+ command rm -fr "${__shunit_tmpDir}"
+ fi
+
+ if ${__SHUNIT_BUILTIN} [ "${_shunit_name_}" != 'EXIT' ]; then
+ # Handle all non-EXIT signals.
+ trap - 0 # Disable EXIT trap.
+ exit ${_shunit_signal_}
+ elif ${__SHUNIT_BUILTIN} [ ${__shunit_reportGenerated} -eq ${SHUNIT_FALSE} ]; then
+ _shunit_assertFail 'unknown failure encountered running a test'
+ _shunit_generateReport
+ exit ${SHUNIT_ERROR}
+ fi
+
+ unset _shunit_name_ _shunit_signal_
+}
+
+# configureColor based on user color preference.
+#
+# Args:
+# color: string: color mode (one of `always`, `auto`, or `never`).
+_shunit_configureColor() {
+ _shunit_color_=${SHUNIT_FALSE} # By default, no color.
+ case $1 in
+ 'always') _shunit_color_=${SHUNIT_TRUE} ;;
+ 'auto')
+ if ${__SHUNIT_BUILTIN} [ "`_shunit_colors`" -ge 8 ]; then
+ _shunit_color_=${SHUNIT_TRUE}
+ fi
+ ;;
+ 'never'|'none') ;; # Support 'none' to support legacy usage.
+ *) _shunit_fatal "unrecognized color option '$1'" ;;
+ esac
+
+ # shellcheck disable=SC2254
+ case ${_shunit_color_} in
+ ${SHUNIT_TRUE})
+ __shunit_ansi_none=${__SHUNIT_ANSI_NONE}
+ __shunit_ansi_red=${__SHUNIT_ANSI_RED}
+ __shunit_ansi_green=${__SHUNIT_ANSI_GREEN}
+ __shunit_ansi_yellow=${__SHUNIT_ANSI_YELLOW}
+ __shunit_ansi_cyan=${__SHUNIT_ANSI_CYAN}
+ ;;
+ ${SHUNIT_FALSE})
+ __shunit_ansi_none=''
+ __shunit_ansi_red=''
+ __shunit_ansi_green=''
+ __shunit_ansi_yellow=''
+ __shunit_ansi_cyan=''
+ ;;
+ esac
+
+ unset _shunit_color_ _shunit_tput_
+}
+
+# colors returns the number of supported colors for the TERM.
+_shunit_colors() {
+ if _shunit_tput_=`${SHUNIT_CMD_TPUT} colors 2>/dev/null`; then
+ echo "${_shunit_tput_}"
+ else
+ echo 16
+ fi
+ unset _shunit_tput_
+}
+
+# The actual running of the tests happens here.
+#
+# Args:
+# None
+_shunit_execSuite() {
+ for _shunit_test_ in ${__shunit_suite}; do
+ __shunit_testSuccess=${SHUNIT_TRUE}
+
+ # Reset per-test info
+ __shunit_assertsCurrentTest=0
+ __shunit_junitXmlCurrentTestCaseErrors=''
+
+ # Disable skipping.
+ endSkipping
+
+ __shunit_startCaseTime=`${__SHUNIT_CMD_DATE_SECONDS}`
+
+ # Execute the per-test setUp() function.
+ if ! setUp; then
+ _shunit_fatal "setUp() returned non-zero return code."
+ fi
+
+ # Execute the test.
+ echo "${__SHUNIT_TEST_PREFIX}${_shunit_test_}"
+ # shellcheck disable=SC2086
+ if ! eval ${_shunit_test_}; then
+ _shunit_error "${_shunit_test_}() returned non-zero return code."
+ __shunit_testSuccess=${SHUNIT_ERROR}
+ fi
+
+ # Execute the per-test tearDown() function.
+ if ! tearDown; then
+ _shunit_fatal "tearDown() returned non-zero return code."
+ fi
+ __shunit_endCaseTime=`${__SHUNIT_CMD_DATE_SECONDS}`
+
+ _shunit_test_execution_time_=`"${__SHUNIT_CMD_CALC}" "${__shunit_endCaseTime}" - "${__shunit_startCaseTime}"`
+
+ # Store current test case info in JUnit XML.
+ __shunit_junitXmlTestCases="${__shunit_junitXmlTestCases}
+ ${__shunit_junitXmlCurrentTestCaseErrors}
+ "
+
+ # Update stats.
+ if ${__SHUNIT_BUILTIN} [ ${__shunit_testSuccess} -eq ${SHUNIT_TRUE} ]; then
+ __shunit_testsPassed=`expr "${__shunit_testsPassed}" + 1`
+ else
+ __shunit_testsFailed=`expr "${__shunit_testsFailed}" + 1`
+ fi
+ done
+
+ unset _shunit_test_ _shunit_test_execution_time_
+}
+
+# Generates the user friendly report with appropriate OK/FAILED message.
+#
+# Args:
+# None
+# Output:
+# string: the report of successful and failed tests, as well as totals.
+_shunit_generateReport() {
+ if ${__SHUNIT_BUILTIN} [ "${__shunit_reportGenerated}" -eq ${SHUNIT_TRUE} ]; then
+ return
+ fi
+
+ _shunit_ok_=${SHUNIT_TRUE}
+
+ # If no exit code was provided, determine an appropriate one.
+ if ${__SHUNIT_BUILTIN} [ "${__shunit_testsFailed}" -gt 0 -o ${__shunit_testSuccess} -eq ${SHUNIT_FALSE} ]; then
+ _shunit_ok_=${SHUNIT_FALSE}
+ fi
+
+ echo
+ _shunit_msg_="Ran ${__shunit_ansi_cyan}${__shunit_testsTotal}${__shunit_ansi_none}"
+ if ${__SHUNIT_BUILTIN} [ "${__shunit_testsTotal}" -eq 1 ]; then
+ ${__SHUNIT_CMD_ECHO_ESC} "${_shunit_msg_} test."
+ else
+ ${__SHUNIT_CMD_ECHO_ESC} "${_shunit_msg_} tests."
+ fi
+
+ if ${__SHUNIT_BUILTIN} [ -n "${__shunit_junitXmlOutputFile}" ]; then
+ # Calculate total execution time in seconds.
+ _shunit_suite_execution_time_=`"${__SHUNIT_CMD_CALC}" "${__shunit_endSuiteTime}" - "${__shunit_startSuiteTime}"`
+
+ # Generate a ISO-8601 compliant date.
+ _shunit_suite_start_time_preformatted_=`date -u '+%Y-%m-%dT%H:%M:%S%z' -d "@${__shunit_startSuiteTime}"`
+
+ echo "
+${__shunit_junitXmlTestCases}
+" > "${__shunit_junitXmlOutputFile}"
+ echo
+ echo "JUnit XML file ${__shunit_junitXmlOutputFile} was saved."
+ fi
+
+ if ${__SHUNIT_BUILTIN} [ ${_shunit_ok_} -eq ${SHUNIT_TRUE} ]; then
+ _shunit_msg_="${__shunit_ansi_green}OK${__shunit_ansi_none}"
+ if ${__SHUNIT_BUILTIN} [ "${__shunit_assertsSkipped}" -gt 0 ]; then
+ _shunit_msg_="${_shunit_msg_} (${__shunit_ansi_yellow}skipped=${__shunit_assertsSkipped}${__shunit_ansi_none})"
+ fi
+ else
+ _shunit_msg_="${__shunit_ansi_red}FAILED${__shunit_ansi_none}"
+ _shunit_msg_="${_shunit_msg_} (${__shunit_ansi_red}failures=${__shunit_assertsFailed}${__shunit_ansi_none}"
+ if ${__SHUNIT_BUILTIN} [ "${__shunit_assertsSkipped}" -gt 0 ]; then
+ _shunit_msg_="${_shunit_msg_},${__shunit_ansi_yellow}skipped=${__shunit_assertsSkipped}${__shunit_ansi_none}"
+ fi
+ _shunit_msg_="${_shunit_msg_})"
+ fi
+
+ echo
+ ${__SHUNIT_CMD_ECHO_ESC} "${_shunit_msg_}"
+ __shunit_reportGenerated=${SHUNIT_TRUE}
+
+ unset _shunit_msg_ _shunit_ok_ _shunit_suite_execution_time_ _shunit_suite_start_time_preformatted_
+}
+
+# Test for whether a function should be skipped.
+#
+# Args:
+# None
+# Returns:
+# boolean: whether the test should be skipped (TRUE/FALSE constant)
+_shunit_shouldSkip() {
+ if ${__SHUNIT_BUILTIN} test ${__shunit_skip} -eq ${SHUNIT_FALSE}; then
+ return ${SHUNIT_FALSE}
+ fi
+ _shunit_assertSkip
+}
+
+# Records a successful test.
+#
+# Args:
+# None
+_shunit_assertPass() {
+ __shunit_assertsPassed=`expr "${__shunit_assertsPassed}" + 1`
+ __shunit_assertsTotal=`expr "${__shunit_assertsTotal}" + 1`
+ __shunit_assertsCurrentTest=`expr "${__shunit_assertsCurrentTest}" + 1`
+}
+
+# Records a test failure.
+#
+# Args:
+# message: string: failure message to provide user
+_shunit_assertFail() {
+ __shunit_testSuccess=${SHUNIT_FALSE}
+ _shunit_incFailedCount
+
+ _shunit_xml_message_="`_shunit_escapeXmlData "$@"`"
+
+ __shunit_junitXmlCurrentTestCaseErrors="${__shunit_junitXmlCurrentTestCaseErrors}
+ "
+
+ if ${__SHUNIT_BUILTIN} [ $# -gt 0 ]; then
+ ${__SHUNIT_CMD_ECHO_ESC} "${__shunit_ansi_red}ASSERT:${__shunit_ansi_none}$*"
+ fi
+
+ unset _shunit_xml_message_
+}
+
+# Increment the count of failed asserts.
+#
+# Args:
+# none
+_shunit_incFailedCount() {
+ __shunit_assertsFailed=`expr "${__shunit_assertsFailed}" + 1`
+ __shunit_assertsTotal=`expr "${__shunit_assertsTotal}" + 1`
+ __shunit_assertsCurrentTest=`expr "${__shunit_assertsCurrentTest}" + 1`
+}
+
+# Records a skipped test.
+#
+# Args:
+# None
+_shunit_assertSkip() {
+ __shunit_assertsSkipped=`expr "${__shunit_assertsSkipped}" + 1`
+ __shunit_assertsTotal=`expr "${__shunit_assertsTotal}" + 1`
+ __shunit_assertsCurrentTest=`expr "${__shunit_assertsCurrentTest}" + 1`
+}
+
+# Dump the current test metrics.
+#
+# Args:
+# none
+_shunit_metrics() {
+ echo "< \
+total: ${__shunit_assertsTotal} \
+passed: ${__shunit_assertsPassed} \
+failed: ${__shunit_assertsFailed} \
+skipped: ${__shunit_assertsSkipped} \
+>"
+}
+
+# Prepare a script filename for sourcing.
+#
+# Args:
+# script: string: path to a script to source
+# Returns:
+# string: filename prefixed with ./ (if necessary)
+_shunit_prepForSourcing() {
+ _shunit_script_=$1
+ case "${_shunit_script_}" in
+ /*|./*) echo "${_shunit_script_}" ;;
+ *) echo "./${_shunit_script_}" ;;
+ esac
+ unset _shunit_script_
+}
+
+# Extract list of functions to run tests against.
+#
+# Args:
+# script: string: name of script to extract functions from
+# Returns:
+# string: of function names
+_shunit_extractTestFunctions() {
+ _shunit_script_=$1
+
+ # Extract the lines with test function names, strip of anything besides the
+ # function name, and output everything on a single line.
+ _shunit_regex_='^\s*((function test[A-Za-z0-9_-]*)|(test[A-Za-z0-9_-]* *\(\)))'
+ grep -E "${_shunit_regex_}" "${_shunit_script_}" \
+ |command sed 's/^[^A-Za-z0-9_-]*//;s/^function //;s/\([A-Za-z0-9_-]*\).*/\1/g' \
+ |xargs
+
+ unset _shunit_regex_ _shunit_script_
+}
+
+# Escape XML data.
+#
+# Args:
+# data: string: data to escape
+# Returns:
+# string: escaped data
+_shunit_escapeXmlData() {
+ # Required XML characters to escape are described here:
+ # http://www.w3.org/TR/REC-xml/#syntax
+ # https://www.liquid-technologies.com/Reference/Glossary/XML_EscapingData.html
+ echo "$*" \
+ |command sed 's/&/\&/g;s/\</g;s/>/\>/g;s/"/\"/g'";s/'/\'/g"
+}
+
+#------------------------------------------------------------------------------
+# Main.
+#
+
+# Determine the operating mode.
+if ${__SHUNIT_BUILTIN} [ $# -eq 0 -o "${1:-}" = '--' ]; then
+ __shunit_script=${__SHUNIT_PARENT}
+ __shunit_mode=${__SHUNIT_MODE_SOURCED}
+else
+ __shunit_script=$1
+ if ! ${__SHUNIT_BUILTIN} [ -r "${__shunit_script}" ]; then
+ _shunit_fatal "unable to read from ${__shunit_script}"
+ fi
+ __shunit_mode=${__SHUNIT_MODE_STANDALONE}
+fi
+
+# Create a temporary storage location.
+__shunit_tmpDir=`_shunit_mktempDir`
+
+# Provide a public temporary directory for unit test scripts.
+# TODO(kward): document this.
+SHUNIT_TMPDIR="${__shunit_tmpDir}/tmp"
+if ! command mkdir "${SHUNIT_TMPDIR}"; then
+ _shunit_fatal "error creating SHUNIT_TMPDIR '${SHUNIT_TMPDIR}'"
+fi
+
+# Configure traps to clean up after ourselves.
+trap '_shunit_cleanup EXIT' 0
+trap '_shunit_cleanup INT' 2
+trap '_shunit_cleanup TERM' 15
+
+# Create phantom functions to work around issues with Cygwin.
+_shunit_mktempFunc
+PATH="${__shunit_tmpDir}:${PATH}"
+
+# Make sure phantom functions are executable. This will bite if `/tmp` (or the
+# current `$TMPDIR`) points to a path on a partition that was mounted with the
+# 'noexec' option. The noexec command was created with `_shunit_mktempFunc()`.
+noexec 2>/dev/null || _shunit_fatal \
+ 'Please declare TMPDIR with path on partition with exec permission.'
+
+# We must manually source the tests in standalone mode.
+if ${__SHUNIT_BUILTIN} [ "${__shunit_mode}" = "${__SHUNIT_MODE_STANDALONE}" ]; then
+ # shellcheck disable=SC1090
+ ${__SHUNIT_BUILTIN} . "`_shunit_prepForSourcing \"${__shunit_script}\"`"
+fi
+
+# Configure default output coloring behavior.
+_shunit_configureColor "${SHUNIT_COLOR}"
+
+__shunit_startSuiteTime=`${__SHUNIT_CMD_DATE_SECONDS}`
+
+# Execute the oneTimeSetUp function (if it exists).
+if ! oneTimeSetUp; then
+ _shunit_fatal "oneTimeSetUp() returned non-zero return code."
+fi
+
+# Command line selected tests or suite selected tests
+if ${__SHUNIT_BUILTIN} [ "$#" -ge 2 ]; then
+ # Argument $1 is either the filename of tests or '--'; either way, skip it.
+ shift
+ # Remaining arguments ($2 .. $#) are assumed to be:
+ # - test function names.
+ # - configuration options, that is started with the `--` prefix.
+ # Interate through all remaining args in "$@" in a POSIX (likely portable) way.
+ # Helpful tip: https://unix.stackexchange.com/questions/314032/how-to-use-arguments-like-1-2-in-a-for-loop
+ for _shunit_arg_ do
+ case "${_shunit_arg_}" in
+ --output-junit-xml=*)
+ # It is a request for JUnit XML output.
+ __shunit_junitXmlOutputFile="${_shunit_arg_#--output-junit-xml=}"
+ ;;
+ --suite-name=*)
+ # It is a request for a custom suite name.
+ __shunit_suiteName="${_shunit_arg_#--suite-name=}"
+ ;;
+ --*)
+ _shunit_fatal "unrecognized option \"${_shunit_arg_}\""
+ ;;
+ *)
+ # It is the test name, process it in a usual way.
+ suite_addTest "${_shunit_arg_}"
+ ;;
+ esac
+ done
+ unset _shunit_arg_
+else
+ # Execute the suite function defined in the parent test script.
+ # DEPRECATED as of 2.1.0.
+ suite
+fi
+
+# If no tests or suite specified, dynamically build a list of functions.
+if ${__SHUNIT_BUILTIN} [ -z "${__shunit_suite}" ]; then
+ shunit_funcs_=`_shunit_extractTestFunctions "${__shunit_script}"`
+ for shunit_func_ in ${shunit_funcs_}; do
+ suite_addTest "${shunit_func_}"
+ done
+fi
+unset shunit_func_ shunit_funcs_
+
+# If suite name is not defined, dynamically generate it from the script name.
+if ${__SHUNIT_BUILTIN} [ -z "${__shunit_suiteName}" ]; then
+ __shunit_suiteName="${__shunit_script##*/}"
+fi
+
+# Prepare the suite name for XML output.
+__shunit_xmlSuiteName="`_shunit_escapeXmlData "${__shunit_suiteName}"`"
+
+# Execute the suite of unit tests.
+_shunit_execSuite
+
+# Execute the oneTimeTearDown function (if it exists).
+if ! oneTimeTearDown; then
+ _shunit_fatal "oneTimeTearDown() returned non-zero return code."
+fi
+
+__shunit_endSuiteTime=`${__SHUNIT_CMD_DATE_SECONDS}`
+
+# Generate a report summary.
+_shunit_generateReport
+
+# That's it folks.
+if ! ${__SHUNIT_BUILTIN} [ "${__shunit_testsFailed}" -eq 0 ]; then
+ return ${SHUNIT_FALSE}
+fi