#!/usr/bin/env bash

# TODO: check that parameters in UPPERCASE are constants (or nearly so ;-)
# ck names: DOCKER_ or CONTAINER_ ? (1st is Docker-specific, and should be used
# always and only for Docker)

DOCKER_IMG_NOT_FOUND="
Could not find Docker image 'sibswiss/chipseq:latest'.

You may want to try:

$ docker pull sibswiss/chipseq:latest
"

APPTAINER_IMG_NOT_FOUND="
Could not find Apptainer/Singularity image 'sibswiss/chipseq:latest'.

You may want to try:

$ apptainer pull docker://sibswiss/chipseq:latest

or

$ singularity pull docker://sibswiss/chipseq:latest
"

APPTAINER_WARNINGS_WARNING="Apptainer/Singularity warnings may interfere with output and cause tests to fail. Pass -W/--no-warnings to override."

META_TEST_WARNING="-M/--meta-tests will cause leftover scripts and outputs. You may want to do a cleanup run (-c)."

get_uname() {
	UNAME="$(uname)"
	case $UNAME in
		Linux | Darwin )
			:
			;;
		* )
			warn "The code was not tested under $UNAME"
			;;
	esac
	echo "$UNAME"
}

docker_cleanup() {
	if [[ $TASK = test ]]; then
		case $CHIPSEQ_TESTS_ENVIRONMENT in
			docker )
				[[ ${CHIPSEQ_TESTS_VERBOSE+x} ]] && printf "Stopping Docker container... "
				if ! docker stop "$docker_container_UUID" > /dev/null; then
					docker kill "$docker_container_UUID"
				fi
				[[ ${CHIPSEQ_TESTS_VERBOSE+x} ]] && printf "done.\nRemoving Docker container.\n"
				docker rm "$docker_container_UUID" > /dev/null
				;;
		esac
	fi
}

help() {
    cat <<-END
Usage: $0 [options]

Options:
-------

    -c, --clean Remove any leftover test scripts (all environments), and test
        output files, then exit successfully. ALL OTHER OPTIONS ARE SILENTLY
        IGNORED.

    -C, --color Colorize output. Default is to NOT to colorize. Equivalent to
        setting the CHIPSEQ_TESTS_COLOR environment variable.

    -e, --environment <native|docker|apptainer|singularity|conda> Run tests in specified
        environment. Can be abbreviated to n, e, a, s, c.
        The only difference between 'singularity' and 'apptainer' is that the
        former will cause the tests to be run by the 'singularity' binary,
        while the latter will use the 'apptainer' binary. The 'singularity' case
        is meant for machines which did not transition from Singularity to
        Apptainer (and hence do not have the 'apptainer' binary).
        For Docker and Apptainer/Singularity, the container should have been
        pulled prior to the tests. For Conda, the chipseq environment must be
        active, and the binaries are expected to reside in
        ~/.conda/envs/chipseq/bin. Default is 'native'.

    -F, --fancy-chars Switch on "fancy" characters like ✓. Default is NOT to use
        fancy characters. Equivalent to setting the CHIPSEQ_TESTS_FANCY_CHARS
        environment variable.

    -f, --force Run tests even if the output directory isn't empty.

    -h, --help Show this help and exit successfully. All other options are
        ignored.

    -o, --keep-outputs Keep output files even when tests are successful.

    -M, --meta-tests Run meta tests, i.e. tests that check that failures are
        being reported. This is equivalent to passing -p 'meta_*' (see -p). This
        WILL LEAVE OUTPUTS in the output directory as well as test scripts in
        the working directory, so you may want to do a cleanup run (-c)
        afterwards, or pass -f to the next run.

    -n, --dry-run do not run the tests (scripts are still produced).

    -p, --pattern <glob> run only tests that match <glob>. See also -M.

    -s, --keep-scripts <always|conditional|never> When (if ever) to keep the test
        scripts. If 'conditional', remove the script IFF the test succeeds. Can
        be abbreviated a, c, or n, respectively. Default is condtional.

    -v, --verbose Be verbose.

    -W, --no-warnings Pass -s to apptainer/singularity, to suppress warnings -
        some tests may fail because of them.  This is NOT passed by default, so
        that any important warnings are not easily missed.

    -x, --xtrace-tests trace tests (set -x on the whole test script)

Return Value
------------

The program returns 0 for success (all tests succeeded) or 1 for anything
else.

Environment Variables
---------------------

    CHIPSEQ_TESTS_COLOR       Output is colorized if set.

    CHIPSEQ_TESTS_FANCY_CHARS Fancy characters are used if set.

END
}

# "maybe" red: that is, red iff color is required. Ditto for the other colors.

mb_red() {
	if [[ ${CHIPSEQ_TESTS_COLOR+x} ]]; then
		printf "%s" "$(red "$1")"
	else
		printf "%s" "$1"
	fi
}

mb_yellow() {
	if [[ ${CHIPSEQ_TESTS_COLOR+x} ]]; then
		printf "%s" "$(yellow "$1")"
	else
		printf "%s" "$1"
	fi
}

mb_green() {
	if [[ ${CHIPSEQ_TESTS_COLOR+x} ]]; then
		printf "%s" "$(green "$1")"
	else
		printf "%s" "$1"
	fi
}

mb_cyan() {
	if [[ ${CHIPSEQ_TESTS_COLOR+x} ]]; then
		printf "%s" "$(cyan "$1")"
	else
		printf "%s" "$1"
	fi
}

parse_args() {
    CHIPSEQ_TESTS_ENVIRONMENT=native
    unset CHIPSEQ_TESTS_VERBOSE
    CHIPSEQ_TESTS_GLOB='test_*'
    unset XTRACE_TESTS
    unset DRY_RUN
    unset KEEP_OUTPUTS
    unset IGNORE_OUTPUTS
    KEEP_SCRIPTS=conditional
	TASK='test'	# Quotes to avoid confusion with test(1)
	# By default, I do NOT suppress apptainer warnings
	unset NO_APPTAINER_WARNINGS

    while (( $# > 0 )); do
        case $1 in
			-C | --color )
				CHIPSEQ_TESTS_COLOR=1
				shift
				;;
			-c | --clean )
				TASK=clean
				shift
				;;
            -e | --environment )
				[[ ${2+x} ]] || die "Option $1 requires an argument (try -h)."
                case $2 in
                    d | docker )
                        CHIPSEQ_TESTS_ENVIRONMENT=docker
						trap docker_cleanup EXIT
                        shift 2
                        ;;
                    a | apptainer )
                        CHIPSEQ_TESTS_ENVIRONMENT=apptainer
                        shift 2
                        ;;
                    s | singularity )
                        # We keep Singularity as a distinct environment for any
                        # machines that run older, pre-renaming code, with a
                        # binary still called 'singularity'.
                        CHIPSEQ_TESTS_ENVIRONMENT=singularity
                        shift 2
                        ;;
                    n | native )
                        # Nothing to do - already default
                        shift 2
                        ;;
					c | conda )
						CHIPSEQ_TESTS_ENVIRONMENT=conda
						shift 2
						;;
                    * )
                        die "$(printf "Unknown test environment '%s'" "$2")"
                        ;;
				esac
                ;;
			-F | --fancy-chars )
				CHIPSEQ_TESTS_FANCY_CHARS=1
				shift
				;;
            -f | --force )
                IGNORE_OUTPUTS=1
                shift
                ;;
            -h | --help )
                help
                exit 0
                ;;
			-M | --meta-tests )
				CHIPSEQ_TESTS_GLOB='meta_*'
				shift
				warn "$META_TEST_WARNING"
				;;
            -n | --dry-run )
                DRY_RUN=1
                shift
                ;;
            -o | --keep-outputs )
                KEEP_OUTPUTS=1
                shift
                ;;
            -p | --pattern )
                CHIPSEQ_TESTS_GLOB=$2
                shift 2
                ;;
            -s | --keep-scripts )
				[[ ${2+x} ]] || die "Option $1 requires an argument (try -h)."
                case $2 in
                    a* )
                        KEEP_SCRIPTS=always
                        ;;
                    c* )
                        KEEP_SCRIPTS=conditional
                        ;;
                    n* )
                        KEEP_SCRIPTS=never
                        ;;
                    * )
                        die "Unrecognized keep-scripts mode $2"
                        ;;
                esac
                shift 2
                ;;
            -v | --verbose )
                export CHIPSEQ_TESTS_VERBOSE=1
                shift
                ;;
			-W | --no-warnings )
				NO_APPTAINER_WARNINGS=1
				shift
				;;
            -x | --xtrace-tests )
                XTRACE_TESTS=1
                shift
                ;;
            -* )
                die "$(printf "Option '%s' not recognized" "$1")"
                ;;
            * )
                # End of options
                break
                ;;
        esac
    done
}

cleanup() {
    OLD_IFS="$IFS"
    IFS=$'\t'
    while read -r name _; do
		for envir in native docker apptainer singularity conda; do
			script="$(script_name "$name" "$envir")"
			[[ ${CHIPSEQ_TESTS_VERBOSE+x} ]] && printf "Considering %s\n" "$script"
			[[ -f $script ]] && rm "$script"
		done
    done < test_cmds.tsv
    IFS="$OLD_IFS"
	local -i nb_out_files
	nb_out_files="$(find "$OUT_DIR" -type f | wc -l)"
	if ((nb_out_files > 0)); then
		rm $OUT_DIR/*
	fi
}

# Sets the output symbols (e.g. a checkmark or an 'ok' string)

set_output_symbols() {
	if [[ ${CHIPSEQ_TESTS_FANCY_CHARS+x} ]] ; then
		OK_SYMBOL=$'\u2714'      # check mark
		EXP_ERROR_SYMBOL='!'
		OTHER_ERROR_SYMBOL='?'
		FAILURE_SYMBOL=$'\u2717' # cross mark
	else
		OK_SYMBOL=ok
		EXP_ERROR_SYMBOL=error
		OTHER_ERROR_SYMBOL=error
		FAILURE_SYMBOL=failure
	fi
}

# Warns if any of the expected files are writeable

check_exp_dir() {
	local PERM_MODE
	case $UNAME in
		Darwin )
			PERM_MODE='+222'
			;;
		* )
			# Assume Linux if not Darwin. May not work, but the user has already
			# been warned (see get_uname()).
			PERM_MODE='/222'
			;;
	esac
	readonly PERM_MODE

    declare -ri num_writeable="$(find "$EXP_DIR" -type f -name '*.exp' -perm "$PERM_MODE" | wc -l)"
    if ((num_writeable != 0)); then
        warn "$EXP_DIR has writeable files"
    fi
}

# Ensures that the output dir exists and has the correct permissions (esp. for
# Docker).

ensure_out_dir() {
    if [[ ! -d "$OUT_DIR" ]]; then
        if ! mkdir "$OUT_DIR"; then
            die "Could not make output directory $OUT_DIR"
        fi
        if ! chmod 777 "$OUT_DIR"; then
            die "Could set correct permissions on $OUT_DIR"
        fi
    else
		# TODO: test $TASK and $IGNORE_OUTPUTS here - only run find if
		# necessary.
		[[ $TASK == clean ]] && return
        nb_out_files="$(find "$OUT_DIR" -type f | wc -l)"
        if [[ ! ${IGNORE_OUTPUTS+x} && $nb_out_files -gt 0 ]]; then
            die "Output directory ($OUT_DIR) not empty, won't clobber (pass -f to override, or -c to cleanup)"
        fi
    fi
}

check_test_name_unicity() {
	# NOTE: the easiest way to do this is with an associative array, but these
	# were introduced to Bash in v. 4.0, and Macs come with v. 3.2, so this
	# isn't an option. We could iterate through every key, but I think it's
	# easier to call sort(1).
	# I use awk(1) rather than cut(1) because MacOSX and Linux versions of
	# cut(1) are too different.
	nb_dup_tests="$(awk -F '\t' '/^test_/ {print $1;}' < test_cmds.tsv | sort | uniq -d | wc -l)"
	if ((nb_dup_tests > 0)); then
		die "Duplicate test names found."
	fi
}

# Checks that containers are available, if environment is Docker or
# Apptainer/Singularity

ensure_container() {
    case $CHIPSEQ_TESTS_ENVIRONMENT in
        native )
            # nothing to do, but I keep this separate from * below to catch any
            # invalid values of CHIPSEQ_TESTS_ENVIRONMENT
            :
            ;;
        docker )
            if ! docker images | grep 'sibswiss/chipseq \+latest' >/dev/null ; then
                die "$DOCKER_IMG_NOT_FOUND"
            fi
            ;;
        apptainer | singularity )
            if [[ ! -f chipseq_latest.sif ]]; then
                die "$APPTAINER_IMG_NOT_FOUND"
            fi
            ;;
		conda )
			# TODO: check that chipseq environment is active
			warn "Check for chipseq env not performed yet."
			;;
        * )
            die "Test environment '$CHIPSEQ_TESTS_ENVIRONMENT' not recognized"
            ;;
    esac
}

# Sets the environment-dependent globals. 

set_global_parameters() {
    case $CHIPSEQ_TESTS_ENVIRONMENT in
        native )
			BIN_DIR='..'
			PERL_BIN_DIR="$BIN_DIR/perl"
			DATA_DIR='../../data'
			;;
		docker )
            BIN_DIR=/usr/local/bin
            PERL_BIN_DIR="$BIN_DIR"
			DATA_DIR=/usr/local/share/chipseq
			;;
		apptainer | singularity )
            BIN_DIR=/usr/bin
            PERL_BIN_DIR="$BIN_DIR"
			DATA_DIR='../../data'
            ;;
		conda )
			BIN_DIR=~/.conda/envs/chipseq/bin
            PERL_BIN_DIR="$BIN_DIR"
			DATA_DIR='../../data'
			;;
		* )
			die "Test environment '$CHIPSEQ_TESTS_ENVIRONMENT' not recognized"
			;;
	esac
    case $CHIPSEQ_TESTS_ENVIRONMENT in
        docker )
            # We pass environment variables to the Docker container using --env on
            # the command line.
            HOST_DATA_DIR="$(readlink -f '../../data')" # Docker needs absolute path
            HOST_TESTS_DIR="$(readlink -f '.')"
			CONTAINER_TESTS_DIR=/home/chipseq/tests
			CONTAINER_DATA_DIR=/home/chipseq/data
			docker_container_UUID="$(docker run                                         \
				--mount type=bind,source="$HOST_DATA_DIR",target=$CONTAINER_DATA_DIR    \
				--mount type=bind,source="$HOST_TESTS_DIR",target=$CONTAINER_TESTS_DIR  \
				--env   BIN_DIR="$BIN_DIR"                                              \
				--env   PERL_BIN_DIR="$PERL_BIN_DIR"                                    \
				--env   DATA_DIR="$DATA_DIR"                                            \
				-d  'sibswiss/chipseq:latest' sleep infinity)"
			readonly docker_container_UUID
			if [[ ${CHIPSEQ_TESTS_VERBOSE+x} ]]; then
				printf "Created docker container %s\n" "$docker_container_UUID"
			fi
            DOCKER_PREFIX="docker exec $docker_container_UUID bash -c "
            readonly HOST_DATA_DIR HOST_TESTS_DIR BIN_DIR PERL_BIN_DIR DATA_DIR \
                DOCKER_PREFIX
            ;;
        native )
            readonly OUT_DIR EXP_DIR BIN_DIR PERL_BIN_DIR DATA_DIR
            ;;
        apptainer | singularity )
			APPTAINER_OPTS=''
			if [[ ${NO_APPTAINER_WARNINGS+x} ]]; then
				APPTAINER_OPTS+=' -s'
			else
				warn "$APPTAINER_WARNINGS_WARNING"
			fi
            readonly HOST_DATA_DIR HOST_TESTS_DIR DATA_DIR
            case $CHIPSEQ_TESTS_ENVIRONMENT in
                apptainer )
                    APPTAINER_PREFIX="apptainer $APPTAINER_OPTS exec chipseq_latest.sif"
                    ;;
                singularity )
                    APPTAINER_PREFIX="singularity $APPTAINER_OPTS exec chipseq_latest.sif"
                    ;;
                * )
                    die "Test environment '$CHIPSEQ_TESTS_ENVIRONMENT' not recognized"
                    ;;
            esac
            readonly CHIPSEQ_TESTS_DIR CHIPSEQ_TESTS_ENV_COMMON_FILE \
                OUT_DIR EXP_DIR
            export CHIPSEQ_TESTS_DIR CHIPSEQ_TESTS_ENV_COMMON_FILE \
                OUT_DIR EXP_DIR
            ;;
        conda )
            CHIPSEQ_TESTS_DIR=.
            readonly CHIPSEQ_TESTS_DIR OUT_DIR EXP_DIR \
                BIN_DIR PERL_BIN_DIR DATA_DIR
            ;;
        * )
            die "Test environment '$CHIPSEQ_TESTS_ENVIRONMENT' not recognized"
            ;;
    esac
}

make_native_script() {
    local -r script=$1
    local -r cmd=$2
    local -r setup=$3
    local -r teardown=$4
    cat > "$script" <<-END
#!/usr/bin/env bash
shopt -s -o nounset
BIN_DIR="$BIN_DIR"
DATA_DIR="$DATA_DIR"
PERL_BIN_DIR="$PERL_BIN_DIR"

$setup

$cmd

$teardown
END
}

# TODO: setup and teardown for environments other than native

make_docker_script() {
    local -r script=$1
    local -r cmd=$2
    local -r setup=$3
    local -r teardown=$4
    cat > "$script" <<-END
#!/usr/bin/env bash
shopt -s -o nounset

$DOCKER_PREFIX '$setup'

$DOCKER_PREFIX '$cmd'

$DOCKER_PREFIX '$teardown'
END
}

make_apptainer_script() {
    local -r script=$1
    local -r cmd=$2
    cat > "$script" <<-END
#!/usr/bin/env bash
shopt -s -o nounset
export BIN_DIR="$BIN_DIR"
export DATA_DIR="$DATA_DIR"
export PERL_BIN_DIR="$PERL_BIN_DIR"

$APPTAINER_PREFIX sh -c '$cmd'
END
}

make_conda_script() {
    local -r script=$1
    local -r cmd=$2
    cat > "$script" <<-END
#!/usr/bin/env bash
shopt -s -o nounset
BIN_DIR="$BIN_DIR"
DATA_DIR="$DATA_DIR"
PERL_BIN_DIR="$PERL_BIN_DIR"


$cmd
END
}

script_name() {
    local -r name=$1
	local -r environment=${2-$CHIPSEQ_TESTS_ENVIRONMENT}
    printf "%s-%s.sh" "$name" "$environment"
}

make_test_scripts() {
    OLD_IFS="$IFS"
    IFS=$'\t'
	# TODO: read the commands file only once, and check the number of fields on
	# that occasion only. Set TWO arrays: one for script names, one for
	# commands. The names array can be used by cleanup() (instead of having that
	# f() read the commands file). Even better, check for test name unicity at
	# that point. In fact, there should be a parse_test_cmds() function that
	# does all the checks and sets (or better, returns (as refs)) the relevant
	# arrays. There should be an array for test names (if we want to keep the
	# order as in the commands file), and hashes for commands (including setup
	# and teardown).
	local -i lineno=0
	# In this case, passing -r to read interfers with the parsing of the commands
	# contained in the TSV, so we'll leave it out.
	# shellcheck disable=SC2162
    while read -a fields; do
		((lineno++))
		[[ ${fields[0]} = \#* ]] && continue # Ignore comment lines
		if (( ${#fields[@]} != 2 && ${#fields[@]} != 4)); then
			printf -v err_msg \
				"test_cmds.csv malformed at line $lineno: - expected 2 or 4 fields (test and command, plus optionally setup and teardown, TAB-separated), got %d" "${#fields[@]}"
			die "$err_msg"
		fi
		name="${fields[0]}"
		test_command="${fields[1]}"
		if ((${#fields[@]} == 4)); then
			setup="${fields[2]}"
			teardown="${fields[3]}"
		else
			setup='# No setup'
			teardown='# No teardown'
		fi
        if [[ $name = $CHIPSEQ_TESTS_GLOB ]]; then
            script="$(script_name "$name")"
            TESTS_LIST+=($name)
            case $CHIPSEQ_TESTS_ENVIRONMENT in
                native )
                    make_native_script "$script" "$test_command" "$setup" "$teardown"
                    ;;
                docker )
					make_docker_script "$script" "$test_command" "$setup" "$teardown"
                    ;;
				apptainer | singularity )
					# Apptainer and Singularity are so similar that the same
					# function is used for both environments.
					make_apptainer_script "$script" "$test_command"
					;;
                conda )
                    make_conda_script "$script" "$test_command"
                    ;;
				* )
					die "Test environment '$CHIPSEQ_TESTS_ENVIRONMENT' not recognized"
					;;
            esac
            chmod 777 "$script"
        fi
    done < test_cmds.tsv
    IFS="$OLD_IFS"
	if ((0 == ${#TESTS_LIST[@]})); then
		warn "$(printf "No tests match '%s'" "$CHIPSEQ_TESTS_GLOB")"
	fi
}

handle_preamble() {
    if [[ ${CHIPSEQ_TESTS_VERBOSE+x} ]]; then
        cat <<- END
		Running tests matching '$CHIPSEQ_TESTS_GLOB' (${#TESTS_LIST[@]} tests).
		Test environment: $CHIPSEQ_TESTS_ENVIRONMENT
END
        [[ ${IGNORE_OUTPUTS+x} ]] && printf "Ignoring any remaining output files.\n"
		[[ ${NO_APPTAINER_WARNINGS+x} ]] && printf \
			"Suppressing Apptainer/Singularity warnings.\n"
    fi
	printf "\n"
}

run_test_scripts() {
    # Get the length of the longest test filename, this is for aligning the
    # test result symbols.
    max_len=$(for n in "${TESTS_LIST[@]}"; do wc -c <<< $n; done | sort -nr | head -1)
    format="Running %-$((max_len+3))s" # + 3 to accomodate '...'
    for test_name in "${TESTS_LIST[@]}"; do
        if [[ ${DRY_RUN+x} ]] ; then
            printf "To run: %s\n" "$test_name"
            continue
        fi
        prefix=${test_name} # TODO: prolly no longer needed
        out_file="$OUT_DIR/$prefix.out"
        exp_file="$EXP_DIR/$prefix.exp"
        test_script="$(script_name "$test_name")"
        printf "$format" "$test_name..."
        # FIXME it should no longer be necessary to distinguish between
        # environments here: all distinctions should be reflected in the
        # test scripts.
        case $CHIPSEQ_TESTS_ENVIRONMENT in
            docker )
				[[ ${XTRACE_TESTS+x} ]] && shopt -s -o xtrace
                ./$test_script > "$out_file"
				exit_code=$?
				shopt -u -o xtrace
                ;;
            native )
                [[ ${XTRACE_TESTS+x} ]] && shopt -s -o xtrace
                export SHELLOPTS
                ./$test_script > "$out_file"
				exit_code=$?
                shopt -u -o xtrace
                ;;
            apptainer | singularity )
                [[ ${XTRACE_TESTS+x} ]] && shopt -s -o xtrace
				./$test_script > "$out_file"
                exit_code=$?
                shopt -u -o xtrace
                ;;
			conda )
				[[ ${XTRACE_TESTS+x} ]] && shopt -s -o xtrace
				export SHELLOPTS
				./$test_script > "$out_file"
				exit_code=$?
				shopt -u -o xtrace
				;;
            * )
                die "Test environment '$CHIPSEQ_TESTS_ENVIRONMENT' not recognized"
                ;;
        esac
		case $exit_code in
			"$RESULT_SUCCESS" )
                if [[ -e $exp_file && -f $exp_file && -r $exp_file ]]; then
                    if diff -q "$out_file" "$exp_file" >/dev/null; then
						printf "%s" "$(mb_green "$OK_SYMBOL")"
                        ((successes++))
                        # Remove output file unless -k/--keep
                        # FIXME: when the output file is written by Docker, its
                        # permissions are different, hence the -f.
                        [[ ${KEEP_OUTPUTS+x} ]] || rm -f "$out_file"
                        [[ $KEEP_SCRIPTS == always ]] || rm "$test_script"
                    else
                        printf "%s" "$(mb_red "$FAILURE_SYMBOL")"
                        ((failures++))
                        [[ $KEEP_SCRIPTS == never ]] && rm "$test_script"
                    fi
                else
                    printf "%s .exp file (%s) not found, not a plain file, or unreadable" \
                        "$(mb_yellow "$EXP_ERROR_SYMBOL")" \
                        "$exp_file"
                    ((errors++))
                    [[ $KEEP_SCRIPTS == never ]] && rm "$test_script"
                fi
				;;
			* )
				printf "%s" "$(mb_yellow "$OTHER_ERROR_SYMBOL")"
                ((errors++))
                [[ $KEEP_SCRIPTS == never ]] && rm "$test_script"
				;;
		esac
		printf "\n"
    done
}

summary() {
    [[ ${DRY_RUN+x} ]] && return 0
    if ((successes + failures + errors != ${#TESTS_LIST[@]})); then
        die "$successes successes + $failures failures + $errors errors don't add up to ${#TESTS_LIST} tests"
    fi
	printf "\n"
	if ((successes == ${#TESTS_LIST[@]})) ; then
		printf "All %d tests %s!\n" "$successes" "$(mb_green OK)"
	else
		printf "%d %s\n" "$successes" "$(mb_green successes)"
		printf "%d %s\n" "$failures" "$(mb_red failures)"
		printf "%d %s\n" "$errors" "$(mb_yellow errors)"
		exit 1
	fi
}

################################################################
# Main

shopt -s -o nounset

source ./common.sh

# These must be declared outside functions, otherwise 'declare' would make them
# local... (they could also not be declared at all, but I like to make things
# explicit).

declare -a TESTS_LIST=()
declare -i successes=0
declare -i failures=0
declare -i errors=0
OUT_DIR=./out
EXP_DIR=./exp

UNAME=$(get_uname)
readonly UNAME

parse_args "$@"

readonly CHIPSEQ_TESTS_ENVIRONMENT CHIPSEQ_TESTS_VERBOSE \
    CHIPSEQ_TESTS_GLOB XTRACE_TESTS DRY_RUN \
    OUT_DIR EXP_DIR SHELLOPTS

ensure_out_dir

#TODO move this into a handle_cleanup() function
case $TASK in
	clean )
		cleanup
		exit 0
		;;
	test )
		# Nothing right now, proceed to main script below.
		:
		;;
	* )
		die "Unexpected TASK '$TASK'"
		;;
esac

set_output_symbols

check_exp_dir

check_test_name_unicity

ensure_container

set_global_parameters

make_test_scripts # Also sets TESTS_LIST

handle_preamble

run_test_scripts

summary
