#!/bin/bash

### Setup we need right away

set -e

# Set the locale so we have predictable sorting. However, ch-image crashes on
# Python 3.6 in locale “C” (see issues #970 and #1262), which is the only
# locale guaranteed to be available, so use a UTF-8 locale if available. This
# means tests will fail on Python 3.6 systems without this UTF-8 locale.
if [[ $(locale -a) = *en_US.utf8* ]]; then
    export LC_ALL=en_US.utf8
else
    export LC_ALL=C
fi

# Set environment variable for ch-image --debug flag
export CH_IMAGE_DEBUG=true

# Set environment variable for ch-convert and ch-image --xattrs flag
export CH_XATTRS=true

### Functions we need right away

fatal () {
    printf 'error: %s\n' "$1" 1>&2
    exit 1
}

warning () {
    printf 'warning: %s\n' "$1" 1>&2
}


### Setup

ch_lib=$(cd "$(dirname "$0")" && pwd)/../lib
if [[ ! -f ${ch_lib}/base.sh ]]; then
    fatal "install or build problem: not found: ${ch_lib}/base.sh"
fi
. "${ch_lib}/base.sh"
. "${ch_lib}/contributors.bash"
export ch_bin
export ch_lib

if docker info > /dev/null 2>&1; then
    ch_docker_nosudo=yes
else
    ch_docker_nosudo=
fi
export ch_docker_nosudo

usage=$(cat <<EOF
Run some or all of the Charliecloud test suite.

Usage:

  $ $(basename "$0") [PHASE] [--scope SCOPE] [--pack-fmt FMT] [OPTIONS]

Valid phases: build-images, build, rootemu, run, examples, all, most, mk-perm-dirs, clean,
              rm-perm-dirs
Valid scopes: quick, standard, full
Valid pack formats: squash-mount, tar-unpack, squash-unpack

Additional options:

  -b,--builder BUILDER   image builder to use
  --dry-run              print summary of what would be tested and exit
  -f,--file FILE[:TEST]  run tests in FILE only, or even just TEST
  -h,--help              print this help and exit
  --img-dir DIR          unpacked images directory; same as \$CH_TEST_IMGDIR
  --fi-path PATH         path to libfabric provider injection target(s)
  --pack-dir DIR         packed image directory; same as \$CH_TEST_TARDIR
  --pack-fmt FMT         packed image format; same as \$CH_TEST_PACK_FMT
  --pedandic {yes,no}    set pedantic mode rather than guessing
  --perm-dir DIR         permissions fixture dir, can repeat; \$CH_TEST_PERMDIRS
  --srun-mpi MPI         srun --mpi argument; see srun --mpi=list
  --sudo                 enable tests that need sudo
  --lustre DIR           run-phase Lustre test directory; \$CH_TEST_LUSTREDIR
  --version              print version and exit

See the man page for important details.
EOF
)


### Global variables

bats_vmin=1.2.0  # FIXME: get from configure somehow?


### Functions

builder_check () {
    printf 'checking builder ...\n'
    case $CH_TEST_BUILDER in
    ch-image)
        if ! "${ch_bin}/ch-image" --dependencies; then
            fatal 'builder: ch-image: missing dependencies'
        fi
        bl=$(readlink -f "${ch_bin}/ch-image")
        bv=$("$bl" --version)
        ;;
    buildah)
        ;&
    buildah-runc)
        ;&
    buildah-setuid)
        if ! command -v buildah > /dev/null 2>&1; then
            fatal 'builder: buildah: not installed.'
        fi
        bl=$(command -v buildah)
        bv=$(buildah --version | awk '{print $3}')
        min='1.11.2'
        ;;
    docker)
        if ! command -v docker > /dev/null 2>&1; then
            fatal 'builder: docker: not installed'
        fi
        bl=$(command -v docker)
        bv=$(docker_ --version | awk '{print $3}' | sed -e 's/,$//')
        ;;
    none)
        bl='none'
        bv=
        ;;
    *)
        fatal "builder: $CH_TEST_BUILDER: not supported"
        ;;
    esac
    printf 'found: %s %s\n\n' "$bl" "$bv"
    version_check 'builder' "$min" "$bv"
}

builder_set () {
    width=$1
    if [[ -n $builder ]]; then
        CH_TEST_BUILDER=$builder
        method='command line'
    elif [[ -n $CH_TEST_BUILDER ]]; then
        method='environment'
    else
        CH_TEST_BUILDER=ch-image
        method='default'
    fi
    printf "%-*s %s (%s)\n" "$width" 'builder:' "$CH_TEST_BUILDER" "$method"
    if [[ $CH_TEST_BUILDER == ch-image ]]; then
        vset CH_IMAGE_STORAGE '' "$CH_IMAGE_STORAGE" "/var/tmp/${USER}.ch" \
                              "$width" 'ch-image storage'
    fi
    export CH_TEST_BUILDER
}

# Create CH_TEST_IMGDIR, avoiding #347.
dir_img_mk () {
    dir_img_rm
    printf "creating %s\n\n" "$CH_TEST_IMGDIR"
    $ch_mpirun_node mkdir "$CH_TEST_IMGDIR"
    $ch_mpirun_node touch "$CH_TEST_IMGDIR/WEIRD_AL_YANKOVIC"
}

dir_img_rm () {
    dir_rm_safe "$CH_TEST_IMGDIR"
}

# Remove a filesystem permissions fixture directory. Ensure that the target
# directory has exactly the two subdirectories expected first.
dir_perm_rm () {
    if [[    $(find "${1}" -maxdepth 1 -mindepth 1 | wc -l) == 2 \
          && -d "${1}/pass" && -d "${1}/nopass" ]]; then
        echo "removing ${1}"
        sudo rm -rf --one-file-system "$1"
    fi
}

# Remove directory $1 if it’s either 1) empty or 2) contains a sentinel file.
dir_rm_safe () {
    if [[    $(find "$1" -maxdepth 1 -mindepth 1 | wc -l) == 0 \
          || -e ${1}/WEIRD_AL_YANKOVIC ]]; then
        echo "removing $1"
        $ch_mpirun_node rm -rf --one-file-system "$1"
    else
        fatal "non-empty and missing sentinel file; manually delete: $1"
    fi
}

# The run phase requires artifacts from a successful build phase. Thus, we
# check sanity based on the minimal set of artifacts (no builder).
dir_tar_check () {
    printf 'checking %s: ' "$CH_TEST_TARDIR"
    dir_tar_check_file chtest{.tar.gz,.sqfs}
    printf 'ok\n\n'
}

dir_tar_check_file () {
    local missing
    for f in "$@"; do
        if [[ -f ${CH_TEST_TARDIR}/${f} ]]; then
            return 0
        else
            missing+=("${CH_TEST_TARDIR}/${f}")
        fi
    done
    fatal "phase $phase: missing packed images: ${missing[*]}"
}

dir_tar_mk () {
    dir_tar_rm
    printf "creating %s\n\n" "$CH_TEST_TARDIR"
    $ch_mpirun_node mkdir "$CH_TEST_TARDIR"
    $ch_mpirun_node touch "${CH_TEST_TARDIR}/WEIRD_AL_YANKOVIC"
}

dir_tar_rm () {
    dir_rm_safe "$CH_TEST_TARDIR"
}

dir_tmp_rm () {
    if [[ $TMP_ == "/tmp/ch-test.tmp.${USER}" ]]; then
        echo "removing $TMP_"
        rm -rf --one-file-system "$TMP_"
    fi
}

dirs_unpriv_rm () {
    dir_tar_rm
    dir_img_rm
    dir_tmp_rm
}

pack_fmt_set () {
    width=$1
    if command -v mksquashfs > /dev/null 2>&1; then
        have_mksquashfs=yes
    else
        have_mksquashfs=
    fi
    if [[ $(ldd "${ch_bin}/ch-run") = *"libsquashfuse"* ]]; then
        have_libsquashfuse=yes
    else
        have_libsquashfuse=
    fi
    if [[ -n $pack_fmt ]]; then
        CH_TEST_PACK_FMT=$pack_fmt
        method='command line'
    elif [[ -n $CH_TEST_PACK_FMT ]]; then
        method='environment'
    elif [[ -n $have_mksquashfs && -n $have_libsquashfuse ]]; then
        CH_TEST_PACK_FMT=squash-mount
        method='default'
    else
        CH_TEST_PACK_FMT=tar-unpack
        method='default'
    fi
    case $CH_TEST_PACK_FMT in
        '🐘')  # elephant emoji U+1F418
            CH_TEST_PACK_FMT=squash-mount
            ;;
        '📠')  # fax machine emoji U+1F4E0
            CH_TEST_PACK_FMT=tar-unpack
            ;;
        '🎃')  # jack-o-lantern emoji U+1F383
            CH_TEST_PACK_FMT=squash-unpack
            ;;
        esac
    export CH_TEST_PACK_FMT
    printf "%-*s %s (%s)\n" \
           "$width" 'packed image format:' "$CH_TEST_PACK_FMT" "$method"
    case $CH_TEST_PACK_FMT in
        squash-mount)
            if [[ -z $have_mksquashfs ]]; then
                fatal "format invalid: ${CH_TEST_PACK_FMT}: no mksquashfs"
            fi
            if [[ -z $have_libsquashfuse ]]; then
                fatal "format invalid: ${CH_TEST_PACK_FMT}: ch-run not linked with libsquashfuse"
            fi
            ;;
        tar-unpack)
            ;;  # nothing to check (assume we have tar)
        squash-unpack)
            if [[ -z $have_mksquashfs ]]; then
                fatal "format invalid: ${CH_TEST_PACK_FMT}: no mksquashfs"
            fi
            ;;
        *)
            fatal "format unknown: ${CH_TEST_PACK_FMT}"
            ;;
    esac
}

pedantry_set () {
    width=$1
    default=no
    # Default to pedantic on CI or if user is a contributor.
    if [[ -n $ch_contributor || -n $CI ]]; then
        default=yes
    fi
    vset ch_pedantic "$pedantic" '' $default "$width" 'pedantic mode'
    if [[ $ch_pedantic == no ]]; then
        ch_pedantic=  # proper boolean
    fi
    # Motivation here: In pedantic mode, we want to run all the tests we
    # reasonably can. So, if the user *has* sudo, then default --sudo to yes.
    # What is a little awkward is that “sudo true” can generate a password
    # prompt in the middle of the status output. An alternative is “sudo -nv”,
    # which doesn’t; drawbacks are that you have to analyze the output (not
    # exit code) and it generates a failed password log message if there is
    # not already a sudo session going. We also used to use “sudo -v”, which
    # prompts for a password even if you have passwordless sudo set up.
    if    [[ -n $ch_pedantic ]] \
       && command -v sudo > /dev/null \
       && sudo true > /dev/null 2>&1; then
        use_sudo_default=yes
    else
        use_sudo_default=
    fi
}

pq_missing () {
    if [[ $phase == all || $phase == build ]]; then
        local img=$1
        local out=$2
        local tag
        tag=$(test_make_auto tag "$img")
        printf '%s\n' "$out" >> "${CH_TEST_TARDIR}/${tag}.pq_missing"
    fi
}

require_unset () {
    name=$1
    value=${!1}
    if [[ -n $value ]]; then
        fatal "$name: no multiple assignment (already \"$value\")"
    fi
}

scope_check () {
    case $1 in
    quick|standard|full)
        return 0
        ;;
    *)
        fatal "invalid scope: $1"
        ;;
    esac
}

# Assign scope a sortable opaque integer value. This value is used to help
# filter images and tests that are out of scope.
scope_to_digit () {
    case $1 in
        quick)
            echo 1
            ;;
        standard)
            echo 2
            ;;
        full)
            echo 3
            ;;
        skip*)
            # skip has the highest value to ensure it is always filtered out
            echo 4
            ;;
        *)
            fatal "scope '$scope' invalid"
            ;;
    esac
}

test_build () {
    echo 'executing build phase tests ...'
    bats build/*.bats
}

test_build_images () {
    echo 'building images ...'
    if [[ ! -f ${TMP_}/build_auto.bats ]]; then
        fatal "${TMP_}/build_auto.bats not found"
    fi
    bats "${TMP_}/build_auto.bats"
}

test_examples () {
    printf '\n'
    if [[ $CH_TEST_SCOPE == quick ]]; then
        echo "no examples for $CH_TEST_SCOPE scope"
    fi
    echo 'executing example phase tests ...'
    if find "$TMP_" -name '*_example.bats' | grep -q .; then
        bats "${TMP_}"/*_example.bats
    fi
}

test_make () {
    local bat_file
    local img_pack
    local pack_files
    local tag
    printf "finding tests compatible with %s phase settings ...\n" "$phase"
    case $phase in
    build|build-images)
        for i in $images $examples; do
           if test_make_check_image "$i"; then
               echo "found: $i"
               build_targets+=( "$i" )
           fi
        done
        printf '\n'
        printf 'generate build_auto.bats ...\n'
        test_make_auto "$phase" "${build_targets[@]}" > "${TMP_}/build_auto.bats"
        printf 'ok\n\n'
        ;;
    run)
        # For each tarball or squashfs file in --pack-dir look for a
        # corresponding example or image that produces a matching tag. If
        # found, check the image for exclusion conidtions.
        pack_files=$(find "$CH_TEST_TARDIR"     -name '*.tar.gz' \
                                            -o  -name '*.sqfs' | sort)
        for i in $pack_files; do
            img_pack=${i##*/}
            img_pack=${img_pack%%.*}
            for j in $images $examples; do
                if [[ $(test_make_tag_from_path "$j") == "$img_pack" ]]; then
                    if test_make_check_image "$j"; then
                        echo "found: $i"
                        run_targets+=( "$j" )
                    fi
                fi
            done
        done
        printf '\n'
        printf 'generate run_auto.bats ...\n'
        test_make_auto "$phase" "${run_targets[@]}" > "${TMP_}/run_auto.bats"
        printf 'ok\n\n'
        ;;
    examples)
        if [[ $CH_TEST_SCOPE == quick ]]; then
            echo 'skipping examples phase in quick scope'
            return
        fi
        for i in $examples; do
            if test_make_check_image "$i"; then
                bat_file=$(dirname "$i")/test.bats
                tag=$(test_make_tag_from_path "$i")
                cp "$bat_file" "${TMP_}/${tag}_example.bats"
                # Substitute $ch_test_tag here with sed because we run all the
                # examples together later, but the value needs to vary between
                # the files. Watch the escaping here.
                sed -i "s/\\\$ch_test_tag/${tag}/g" \
                       "${TMP_}/${tag}_example.bats"
                echo "found: $(dirname "$i")"
            fi
        done
        printf '\n'
        printf 'generate example bat files ...\n'
        printf 'ok\n\n'
        ;;
    *)
        ;;
    esac
}

test_make_auto () {
    local mode
    mode=$1;shift
    if [[ $mode != tag ]]; then
        printf "# Do not edit this file; it's autogenerated\n\n"
        printf "load %s/common.bash\n\n" "$CHTEST_DIR"
    fi
    while [[ "$#" -gt 0 ]]; do
        path_=$1;shift
        basename_=$(basename "$path_")
        dirname_=$(dirname "$path_")
        tag=$(test_make_tag_from_path "$path_")

        if [[ $dir == "" ]];then
            dir='.'
        fi

        if [[ $mode == tag ]]; then
            echo "$tag"
            exit 0
        fi

        if [[ $mode == build* ]]; then
            case $basename_ in
                Build|Build.*)
                    test_make_template_print 'build_custom.bats.in'
                    ;;
                Dockerfile|Dockerfile.*)
                    test_make_template_print 'build.bats.in'
                    test_make_template_print 'builder_to_archive.bats.in'
                    ;;
                *)
                    fatal "test_make_auto: unknown build type"
                    ;;
            esac
        elif [[ $mode == run ]];then
            test_make_template_print 'unpack.bats.in'
        else
             fatal "test_make_auto: invalid mode '$mode'"
        fi
    done
}

test_make_check_image () {
    img_ok=yes
    img=$1
    dir=$(basename "$(dirname "$img")")

    arch_exclude=$(cat "$img" | grep -F 'ch-test-arch-exclude: ' \
                              | sed 's/.*: //' | awk '{print $1}')

    builder_include=$(cat "$img" | grep -F 'ch-test-builder-include: ' \
                                 | sed 's/.*: //' | awk '{print $1}')

    builder_exclude=$(cat "$img" | grep -F 'ch-test-builder-exclude: ' \
                                 | sed 's/.*: //' | awk '{print $1}')

    img_scope_str=$(cat "$img" | grep -F 'ch-test-scope' \
                               | sed 's/.*: //' \
                               | awk '{print $1}')
    [[ -n $img_scope_str ]] || fatal "no scope: $img"
    img_scope_int=$(scope_to_digit "$img_scope_str")
    [[ -n $img_scope_int ]] || exit 1  # set -e not working, why?

    sudo_required=$(cat "$img" | grep -F 'ch-test-need-sudo')

    if [[ $phase == 'build' ]]; then
        # Exclude Dockerfiles if we have no builder.
        if [[ $CH_TEST_BUILDER == none && $img == *Dockerfile* ]]; then
            pq_missing "$img" 'builder required'
            img_ok=
        fi
        # Exclude if included builders are given and $CH_TEST_BUILDER isn’t one.
        if [[ -n $builder_include ]]; then
            builder_ok=
            for b in $builder_include; do
                if [[ $b == "$CH_TEST_BUILDER" ]]; then
                    builder_ok=yes
                fi
            done
            if [[ -z $builder_ok ]]; then
                pq_missing "$img" "builder not included: ${CH_TEST_BUILDER}"
                img_ok=
            fi
        fi
        # Exclude images that are not compatible with CH_TEST_BUILDER.
        for b in $builder_exclude; do
            if [[ $b == "$CH_TEST_BUILDER" ]]; then
                pq_missing "$img" "builder excluded: ${CH_TEST_BUILDER}"
                img_ok=
            fi
        done
    fi

    # Exclude images with a scope that is not a subset of CH_TEST_SCOPE.
    if [[ $scope_int -lt "$img_scope_int" ]]; then
        pq_missing "$img" "not in scope: ${CH_TEST_SCOPE}"
        img_ok=
    fi

    # Exclude images that do not work with the host architecture.
    for a in $arch_exclude; do
        if [[ $a == "$(uname -m)" ]]; then
            pq_missing "$img" "incompatible architecture: ${a}"
            img_ok=
        fi
    done

    # Exclude images that require sudo if CH_TEST_SUDO is empty
    if [[ -n $sudo_required && -z $CH_TEST_SUDO ]]; then
        pq_missing "$img" 'generic sudo required'
        img_ok=
    fi

    # In examples phase, exclude chtest and any images not in a subdirectory of
    # examples.
    if [[ $phase == examples && ( $dir == chtest || $dir == examples ) ]]; then
        img_ok=
    fi

    if [[ -n $img_ok ]]; then
        return 0  # include image
    else
        return 1  # exclude image
    fi
}

test_make_tag_from_path () {
    # Generate a tag from given path.
    #
    # Consider the following path: $CHTEST/examples/Dockerfile.openmpi
    # First break the path into four components:
    #   1) dir:        the parent directory of the file (examples)
    #   2) base:       the full file name (Dockerfile.openmpi)
    #   3) basicname:  the file name (Dockerfile)
    #   4) extension:  the file's extension (openmpi)
    #
    # $basicname must be “Build” or “Dockerfile”; otherwise error.
    # if $dir is “.”, “test”, or “examples” then tag=$extension; otherwise
    # tag is $extension if set or $dir-$exenstion.
    local base
    local basicname
    local dir
    local extension
    local tag
    dir=$(basename "$(dirname "$1")")  # last directory only
    base=$(basename "$1")
    basicname=${base%%.*}
    extension=${base##*.}

    if [[ $extension == "$basicname" ]]; then
        extension=''
    else
        extension=${extension/\./} # remove dot
    fi

    case $basicname in
        Build|Dockerfile)
            case $dir in
                .|examples|charliecloud|test)  # dot is directory “test”
                    if [[ -z $extension ]]; then
                        fatal "can't compute tag: $1"
                    else
                        tag=$extension
                    fi
                    ;;
                *)
                    if [[ -z $extension ]]; then
                        tag=$(basename "$dir")
                    else
                        tag=$(basename "${dir}-${extension}")
                    fi
            esac
            ;;
        *)
            fatal "test_make_auto: invalid basic name '$basicname'"
            ;;
    esac
    echo "$tag"
}

test_make_template_print () {
    local template
    template="./make-auto.d/$1"

    # Subsitute template variables and remove “source” command that is only
    # for ShellCheck.
      cat "$template" \
    | sed -E -e 's/^(source common\.bash.*)$/#\1/' \
             -e "s@%\(basename\)s@$basename_@g" \
             -e "s@%\(dirname\)s@$dirname_@g" \
             -e "s@%\(path\)s@$path_@g" \
             -e "s@%\(scope\)s@$CH_TEST_SCOPE@g" \
             -e "s@%\(tag\)s@$tag@g"
    printf '\n'
}

test_one_file () {
    if [[ $one_file != *.bats ]]; then
        # Assume it's a Dockerfile or Build file; derive tag and test.bats.
        ch_test_tag=$(test_make_tag_from_path "$one_file")
        export ch_test_tag
        one_file=$(dirname "$one_file")/test.bats
        printf 'tag:   %s\n' "$ch_test_tag"
    fi
    printf 'file:  %s\n' "$one_file"
    bats "$one_file"
}

test_rootemu () {
    if command -v ch-image > /dev/null 2>&1; then
        bats force-auto.bats
        echo yes > "$TMP_/rootemu"
    elif [[ -n "$1" ]]; then
        printf "error: ch-image required, not found\n"
        exit 1
    fi
}

test_run () {
    echo 'executing run phase tests ...'
    if [[ ! -f ${TMP_}/run_auto.bats ]]; then
        fatal "${TMP_}/run_auto.bats not found"
    fi
    bats run_first.bats "${TMP_}/run_auto.bats" ./run/*.bats
    if [[ $CH_TEST_SCOPE != quick ]]; then
        for guest_user in $(id -un) root nobody; do
            for guest_group in $(id -gn) root $(id -gn nobody); do
                export GUEST_USER=$guest_user
                export GUEST_GROUP=$guest_group
                echo "testing as $GUEST_USER $GUEST_GROUP"
                bats run/ch-run_uidgid.bats
            done
        done
    fi
}

# Exit with failure if given version number is below a minimum.
#
# $1: human-readable descriptor
# $2: minimum version
# $3: actual version
version_check () {
    desc=$1
    min=$2
    actual=$3
    if [[ $(  printf '%s\n%s\n' "$min" "$actual" \
            | sort -V | head -n1) != "$min" ]]; then
        fatal "$desc: mininum version $min, found $actual"
    fi
}

win () {
    printf "\nAll tests passed.\n"
}


### Body of script

# Ensure ch-run has been compiled (issue #329).
if ! "${ch_bin}/ch-run" --version > /dev/null 2>&1; then
    fatal "no working ch-run found in $ch_bin"
fi

# Some tests have specific libc requirements.
case $(readelf -p .interp "${ch_bin}/ch-run") in
    *musl*)            # e.g. /lib/ld-musl-x86_64.so.1
        export ch_libc=musl
        ;;
    *)                 # e.g. /lib64/ld-linux-x86-64.so.2
        export ch_libc=glibc
        ;;
esac

# Ensure we have Bash 4.1 or higher
if /bin/bash -c 'set -e; [[ 1 = 0 ]]; exit 0'; then
    # Bash bug: [[ ... ]] expression doesn't exit with set -e
    # https://github.com/sstephenson/bats/issues/49
    fatal 'Bash minimum version is 4.1'
fi

# Is the user a contributor?
email=
# First, ask Git for the configured e-mail address.
if command -v git > /dev/null 2>&1; then
    email="$(git config --get user.email || true)"
fi
# If that doesn’t work, construct it from the environment.
if [[ -z $email ]]; then
    email="$USER@$(hostname --domain)"
fi
ch_contributor=
for i in "${ch_contributors[@]}"; do
    if [[ $i == "$email" ]]; then
        ch_contributor=yes
    fi
done

# Ensure Bats is installed.
if command -v bats > /dev/null 2>&1; then
    bats=$(command -v bats)
    bats_version="$(bats --version | awk '{print $2}')"
else
    fatal 'Bats not found'
fi

# Reject non-default registry on GitHub Actions.
if [[ -n $GITHUB_ACTIONS && -n $CH_REGY_DEFAULT_HOST ]]; then
    fatal 'non-default registry on GitHub Actions invalid'
fi

# Create a directory to hold auto-generated test artifacts.
TMP_=/tmp/ch-test.tmp.$USER
if [[ ! -d $TMP_ ]]; then
    mkdir "$TMP_"
    chmod 700 "$TMP_"
fi

# Record that we haven’t (yet) run the “rootemu” tests
echo no > "$TMP_/rootemu"

# Find test directories. Note some of this gets rewritten at install time.
CHTEST_DIR=${ch_base}/test
CHTEST_EXAMPLES_DIR=${ch_base}/examples
if [[ ! -f ${ch_base}/VERSION ]]; then
    # installed
    CHTEST_INSTALLED=yes
    CHTEST_GITWD=
else
    # build dir
    CHTEST_INSTALLED=
    if [[ -e ${ch_base}/.git ]]; then
        CHTEST_GITWD=yes
    else
        CHTEST_GITWD=
    fi
fi
export ch_base
export CHTEST_INSTALLED
export CHTEST_GITWD
export CHTEST_DIR
export CHTEST_EXAMPLES_DIR
export TMP_

# Check for test directory.
if [[ ! -d $CHTEST_DIR ]]; then
    fatal "test directory not found: $CHTEST_DIR"
fi
if [[ ! -d $CHTEST_EXAMPLES_DIR ]]; then
    fatal "examples not found: $CHTEST_EXAMPLES_DIR"
fi

# Parse arguments.
if [[ $# == 0 ]]; then
    usage 1
fi
while [[ $# -gt 0 ]]; do
    opt=$1; shift
    case $opt in
    all|build|build-images|clean|examples|mk-perm-dirs|rm-perm-dirs|rootemu|run)
        require_unset phase
        phase=$opt
        ;;
    -b|--builder)
        require_unset builder
        builder=$1; shift
        ;;
    --builder=*)
        require_unset builder
        builder=${opt#*=}
        ;;
    -c|--bucache-mode)
        require_unset bucache_mode
        bucache_mode=$1; shift
        ;;
    --bucache-mode=*)
        require_unset bucache_mode
        bucache_mode=${opt#*=}
        ;;
    --dry-run)
        dry=true
        ;;
    -f|--file)
        require_unset phase
        phase=one-file
        one_file=$1; shift
        ;;
    --file=*)
        require_unset phase
        phase=one-file
        one_file=${opt#*=}
        ;;
    -h|--help)
        usage 0
        ;;
    --img-dir)
        require_unset imgdir
        imgdir=$1; shift
        ;;
    --img-dir=*)
        require_unset imgdir
        imgdir=${opt#*=}
        ;;
    --is-pedantic)  # undocumented; for CI
        is_pedantic=yes
        ;;
    --is-sudo)      # undocumented; for CI
        is_sudo=yes
        ;;
    --fi-path)
        require_unset fi_path
        fi_path=$1; shift
        ;;
    --fi-path=*)
        require_unset fi_path
        fi_path=${opt#*=}
        ;;
    --pack-dir)
        require_unset tardir
        tardir=$1; shift
        ;;
    --pack-dir=*)
        require_unset tardir
        tardir=${opt#*=}
        ;;
    --pack-fmt)
        require_unset pack_fmt
        pack_fmt=$1; shift
        ;;
    --pack-fmt=*)
        require_unset pack_fmt
        pack_fmt=${opt#*=}
        ;;
    --pedantic)
        pedantic=$1; shift
        ;;
    --pedantic=*)
        pedantic=${opt#*=}
        ;;
    --perm-dir)
        use_sudo=yes
        permdirs+=("$1"); shift
        ;;
    --perm-dir=*)
        use_sudo=yes
        permdirs+=("${opt#*=}")
        ;;
    -s|--scope)
        require_unset scope
        scope_check "$1"
        scope=$1; shift
        ;;
    --scope=*)
        require_unset scope
        scope=${opt#*=}
        scope_check "$scope"
        ;;
    --sudo)
        use_sudo=yes
        ;;
    --srun-mpi=*)
        require_unset srun_mpi
        srun_mpi="${opt#*=}"
        ;;
    --srun-mpi)
        require_unset srun_mpi
        srun_mpi=$1; shift
        ;;
    --lustre)
        require_unset lustredir
        lustredir=$1; shift
        ;;
    --lustre=*)
        require_unset lustredir
        lustredir=${opt#*=}
        ;;
    --version)
        version
        ;;
    *)
        fatal "unrecognized argument: $opt"
        ;;
    esac
done

printf 'ch-run:       %s (%s)\n' "${ch_bin}/ch-run" "$ch_libc"
printf 'bats:         %s (%s)\n' "$bats" "$bats_version"
if [[    $(printf "%s\n%s" "$bats_version" "$bats_vmin" | sort -V | head -1) \
      != "$bats_vmin" ]]; then
    warning "Bats version unsupported b/c < $bats_vmin"
fi
printf 'tests:        %s\n' "$CHTEST_DIR"
printf 'installed:    %s\n' "${CHTEST_INSTALLED:-no}"
printf 'locale:       %s\n' "$LC_ALL"
printf 'git workdir:  %s\n' "${CHTEST_GITWD:-no}"
if [[ -n $ch_contributor ]]; then
    ch_contributor_note="yes; $email in README.rst"
else
    ch_contributor_note="no; $email not in README.rst"
fi
printf 'contributor:  %s\n\n' "$ch_contributor_note"

if [[ $phase = one-file ]]; then
    if [[ $one_file == *:* ]]; then
        x=$one_file
        one_file=${x%%:*}            # before first colon
        export ch_one_test=${x#*:}   # after first colon
    fi
    if [[ ! -f $one_file ]]; then
        fatal "not a file: $one_file"
    fi
    one_file=$(readlink -f "$one_file")  # make absolute b/c we cd later
    if [[ $one_file = */test.bats ]]; then
        fatal '--file: must specify build recipe file, not test.bats'
    fi
fi

printf "%-21s %s" 'phase:' "$phase"
if [[ $phase = one-file ]]; then
    printf ': %s (%s)' "$one_file" "$ch_one_test"
fi
if [[ -z $phase ]]; then
    fatal 'phase: no phase specified'
fi
printf '\n'

# See issue #1580
# shellcheck disable=SC2016
if [[ -d /var/tmp/img ]] || [[ -d /var/tmp/tar ]]; then
printf '\n'
  warning 'NOTE: default image and pack directories changed to:'
  warning '      CH_TEST_IMGDIR=/var/tmp/${USER}.img'
  warning '      CH_TEST_TARDIR=/var/tmp/${USER}.pack'
fi

printf '\n'

#    variable name    CLI              environment         default
#                     desc. width  description
vset CH_TEST_SCOPE    "$scope"         "$CH_TEST_SCOPE"    standard \
                      21 'scope'
builder_set 21
pedantry_set 21
vset CH_TEST_SUDO     "$use_sudo"      "$CH_TEST_SUDO"     "$use_sudo_default" \
                      21 'use generic sudo'
vset CH_TEST_IMGDIR   "$imgdir"        "$CH_TEST_IMGDIR"   "/var/tmp/${USER}.img" \
                      21 'unpacked images dir'
vset CH_TEST_TARDIR   "$tardir"        "$CH_TEST_TARDIR"   "/var/tmp/${USER}.pack" \
                      21 'packed images dir'
pack_fmt_set 21
vset CH_IMAGE_CACHE   "$bucache_mode"  "$CH_IMAGE_CACHE"   enabled \
                      21 'build cache mode'
vset CH_TEST_PERMDIRS "${permdirs[*]}" "$CH_TEST_PERMDIRS" skip \
                      21 'fs permissions dirs'
vset CH_TEST_LUSTREDIR "$lustredir"    "$CH_TEST_LUSTREDIR" skip \
                      21 'Lustre test dir'
vset CH_TEST_SLURM_MPI "$srun_mpi"     "$CH_TEST_SLURM_MPI" "$SLURM_MPI_TYPE" \
                      21 'srun mpi type'
vset CH_TEST_OFI_PATH  "$fi_path"     "$CH_TEST_OFI_PATH"  skip \
                      21 'ofi provider(s) path'
printf '\n'

if [[ $phase == *'perm'* ]] && [[ $CH_TEST_PERMDIRS == skip ]]; then
    fatal "phase $phase: CH_TEST_PERMDIRS: can't be 'skip'"
fi

# Check that different sources of version number are consistent.
printf 'ch-test version: %s\n' "$ch_version"
ch_run_version=$("${ch_bin}/ch-run" --version 2>&1)
if [[ $ch_version != "$ch_run_version" ]]; then
    warning "inconsistent ch-run version: ${ch_run_version}"
fi
if [[ -z $CHTEST_INSTALLED ]]; then
    cf_version=$("${ch_base}/configure" --version | head -1 | cut -d' ' -f3)
    if [[ $ch_version != "$cf_version" ]]; then
        warning "inconsistent configure version: ${cf_version}"
    fi
    # Charliecloud version. Prefer git; otherwise use simple version.
    src_version=$(   "${ch_base}/misc/version" 2> /dev/null \
                  || cat "${ch_base}/VERSION")
    if [[ $ch_version != "$src_version" ]]; then
        warning "inconsistent source version: ${src_version}"
    fi
fi
printf '\n'

# Ensure ofi path looks sane.
if [[ -n "$fi_path" ]]; then
    case "$fi_path" in
        *-fi.so)
        true
     ;;
    *libfabric.so)
        true
    ;;
     *)
    fatal '--fi-path: must end in -fi.so or libfabric.so'
    esac
fi

# Don't allow FI_ variables.
if [[ -n "$FI_PROVIDER" ]]; then
    fatal 'host FI_PROVIDER set'
fi
if [[ -n "$FI_PROVIDER_PATH" ]]; then
    fatal 'host FI_PROVIDER_PATH set'
fi

# If srun --mpi=pmix, set variable to avoid spurious error. See
# https://github.com/open-mpi/ompi/issues/7516.
if [[ "$CH_TEST_SLURM_MPI" == 'pmix'* ]]; then
    export PMIX_MCA_gds=hash
fi

# Ensure BATS_TMPDIR is set to /tmp (issue #278).
if [[ -n $BATS_TMPDIR && $BATS_TMPDIR != '/tmp' ]]; then
    fatal "BATS_TMPDIR: must be /tmp; found '$BATS_TMPDIR' (issue #278)"
fi

# Ensure namespaces are configured properly.
printf 'checking namespaces ...\n'
if ! "${ch_bin}/ch-checkns"; then
    fatal 'namespace sanity check (ch-checkns) failed'
fi
printf '\n'

if [[ $CH_TEST_SUDO ]]; then
    printf 'checking sudo ...\n'
    sudo echo ok
    printf '\n'
fi

if [[ -n $is_pedantic ]]; then
    printf 'exiting per --is-pedantic\n'
    if [[ -n $ch_pedantic ]]; then exit 0; else exit 1; fi
fi

if [[ -n $is_sudo ]]; then
    printf 'exiting per --is-sudo\n'
    if [[ -n $CH_TEST_SUDO ]]; then exit 0; else exit 1; fi
fi

if [[ -n $dry ]];then
    printf 'exiting per --dry-run\n'
    exit 0
fi

cd "$CHTEST_DIR"

export PATH=$ch_bin:$PATH

# Now that CH_TEST_* variables, PATH, and BATS_TMPDIR has been set and checked,
# we source CHTEST_DIR/common.bash.
. "${CHTEST_DIR}/common.bash"

# The distinction here is that “images” are purely for testing and have no
# value as examples for the user, while “examples” are dual-purpose. We call
# “find” twice for each to preserve desired sort order.
images=$(     find "$CHTEST_DIR"             -name 'Dockerfile.*' | sort \
           && find "$CHTEST_DIR"             -name 'Build' \
                                          -o -name 'Build.*' | sort)
examples=$(   find "$CHTEST_EXAMPLES_DIR"    -name 'Dockerfile' \
                                          -o -name 'Dockerfile.*' | sort \
           && find "$CHTEST_EXAMPLES_DIR"    -name 'Build' \
                                          -o -name 'Build.*' | sort)

scope_int=$(scope_to_digit "$CH_TEST_SCOPE")

# Execute phase
case $phase in
    all)
        phase=build
        dir_tar_mk
        builder_check
        test_make
        test_build_images
        test_build

        phase=rootemu
        if [[ "$scope" != "full" ]]; then
            echo "skipping root emulation tests (full scope only)"
        else
            echo 'executing root emulation tests...'
            test_rootemu
        fi

        phase=run
        dir_img_mk
        dir_tar_check
        test_make
        test_run

        phase=examples
        dir_tar_check
        test_make
        test_examples

        # Kobe.
        win
        ;;
    build)
        builder_check
        dir_tar_mk
        test_make
        test_build_images
        test_build
        win
        ;;
    clean)
        dirs_unpriv_rm
        if [[ -d $TMP_ ]] && [[ -e $TMP_/build_auto.bats ]]; then
            echo "removing $TMP_"
            rm -rf --one-file-system "$TMP_"
        fi
        ;;
    examples)
        test_make
        test_examples
        win
        ;;
    build-images)
        builder_check
        dir_tar_mk
        test_make
        test_build_images
        win
        ;;
    mk-perm-dirs)
        printf 'creating filesystem permissions fixtures ...\n'
        for d in $CH_TEST_PERMDIRS; do
            if [[ -d ${d} ]]; then
                printf '%s already exists\n' "$d"
                continue
            else
                sudo "${CHTEST_DIR}/make-perms-test" "$d" "$USER" nobody
            fi
        done
        echo
        ;;
    one-file)
        test_one_file
        win
        ;;
    rm-perm-dirs)
        for d in $CH_TEST_PERMDIRS; do
            dir_perm_rm "$d"
        done
        ;;
    rootemu)
        test_rootemu optional # ch-image optional
        ;;
    run)
        dir_img_mk
        test_make
        test_run
        win
        ;;
esac
