#!/bin/bash
# THIS FILE IS PART OF THE CYLC SUITE ENGINE.
# Copyright (C) 2008-2016 NIWA
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# NAME
#     test_header
#
# SYNOPSIS
#     . $CYLC_DIR/t/lib/bash/test_header
#
# DESCRIPTION
#     Interface for constructing tests under a TAP harness (the "prove"
#     command).
#
# FUNCTIONS
#     set_test_number N
#         echo a total number of tests for TAP to read.
#     ok TEST_NAME
#         echo a TAP OK message for TEST_NAME.
#     fail TEST_NAME
#         echo a TAP fail message for TEST_NAME. If $CYLC_TEST_DEBUG is set,
#         cat $TEST_NAME.stderr to stderr.
#     run_ok TEST_NAME COMMAND ...
#         Run COMMAND with any following options/arguments and store stdout
#         and stderr in TEST_NAME.stdout and TEST_NAME.stderr.
#         This is expected to have a return code of 0, which ok's the test.
#     run_fail TEST_NAME COMMAND ...
#         Run COMMAND with any following options/arguments and store stdout
#         and stderr in TEST_NAME.stdout and TEST_NAME.stderr.
#         This is expected to have a non-zero return code, which ok's the test.
#     cmp_ok FILE_TEST [FILE_CONTROL]
#         Compare FILE_TEST with a file or stdin given by FILE_CONTROL (stdin
#         if FILE_CONTROL is "-" or missing). By default, it uses "diff -u" to
#         compare files. However, if an alternate command such as "xxdiff -D"
#         is desirable (e.g. for debugging),
#         "export CYLC_TEST_DIFF_CMD=xxdiff -D".
#     contains_ok FILE_TEST [FILE_CONTROL]
#         Make sure that each line in FILE_TEST is present in FILE_CONTROL
#         (stdin if FILE_CONTROL is "-" or missing).
#     grep_ok PATTERN FILE
#         Run "grep -q PATTERN FILE".
#     greps_ok PATTERNS FILE
#         Runs "grep -p PATTERN FILE" where PATTERN is one line of PATTERNS,
#         for each line in PATTERNS.
#     count_ok PATTERN FILE COUNT
#         Test that PATTERN occurs in exactly COUNT lines of FILE.
#     exists_ok FILE
#         Test that FILE exists
#     exists_fail FILE
#         Test that FILE does not exist
#     init_suite SUITE_NAME
#         Create a suite called '__cylc__test__${SUITE_NAME}__' in $TEST_DIR.
#     mock_smtpd_init
#         Start a mock SMTP server daemon for testing. Write host:port setting
#         to the variable TEST_SMTPD_HOST. Write pid of daemon to
#         TEST_SMTPD_PID. Write log to TEST_SMTPD_LOG.
#     mock_smtpd_kill
#         Kill the mock SMTP server daemon process.
#     purge_suite SUITE_NAME
#         Tidy up test directories for SUITE_NAME.
#     poll COMMAND
#         Run COMMAND in 1 second intervals for a minute until COMMAND returns
#         a non-zero value.
#     skip N SKIP_REASON
#         echo "ok $((++T)) # skip SKIP_REASON" N times.
#     skip_all SKIP_REASON
#         echo "1..0 # SKIP SKIP_REASON" and exit.
#     ssh_install_cylc HOST
#         Install cylc on a remote host.
#     create_test_globalrc [PRE [POST]]
#         Create a new global config file $PWD/conf from site global-tests.rc
#         with PRE and POST pre- and ap-pended (PRE for top level items with no
#         section heading).
#-------------------------------------------------------------------------------
set -eu

FAILURES=0
SIGNALS="EXIT INT"
TEST_DIR=
TEST_RHOST_CYLC_DIR=
function FINALLY() {
    for S in $SIGNALS; do
        trap '' $S
    done
    if [[ -n $TEST_DIR ]]; then
        cd ~
        rm -rf $TEST_DIR
    fi
    if [[ -n "${TEST_RHOST_CYLC_DIR}" ]]; then
        ssh -oBatchMode=yes -oConnectTimeout=5 "${TEST_RHOST_CYLC_DIR%%:*}" \
            "rm -fr ${TEST_RHOST_CYLC_DIR#*:}"
    fi
    if [[ -n "${TEST_SMTPD_PID:-}" ]]; then
        kill "${TEST_SMTPD_PID}"
    fi
    if (($FAILURES > 0)); then
        echo -e "\n    stdout and stderr stored in: $TEST_LOG_DIR" >&2
        if $SUITE_RUN_FAILS; then
            echo -e "    suite logs can be found under: $SUITE_LOG_DIR" >&2
        fi
    fi
}
for S in $SIGNALS; do
    trap "FINALLY $S" $S
done

TEST_NUMBER=0

function set_test_number() {
    echo "1..$1"
}

function ok() {
    echo "ok $((++TEST_NUMBER)) - $@"
}

function fail() {
    ((++FAILURES))
    echo "not ok $((++TEST_NUMBER)) - $@"
    if [[ -n ${CYLC_TEST_DEBUG:-} ]]; then
        echo >/dev/tty
        echo "$TEST_NAME_BASE $TEST_NAME" >/dev/tty
        cat "$TEST_NAME.stderr" >/dev/tty
    fi
}

function run_ok() {
    local TEST_NAME=$1
    shift 1
    if ! "$@" 1>$TEST_NAME.stdout 2>$TEST_NAME.stderr; then
        fail $TEST_NAME
        mkdir -p $TEST_LOG_DIR
        cp $TEST_NAME.stdout $TEST_LOG_DIR/$TEST_NAME.stdout
        cp $TEST_NAME.stderr $TEST_LOG_DIR/$TEST_NAME.stderr
        return
    fi
    ok $TEST_NAME
}

function run_fail() {
    local TEST_NAME=$1
    shift 1
    if "$@" 1>$TEST_NAME.stdout 2>$TEST_NAME.stderr; then
        fail $TEST_NAME
        mkdir -p $TEST_LOG_DIR
        cp $TEST_NAME.stdout $TEST_LOG_DIR/$TEST_NAME.stdout
        cp $TEST_NAME.stderr $TEST_LOG_DIR/$TEST_NAME.stderr
        return
    fi
    ok $TEST_NAME
}

function suite_run_ok() {
    local TEST_NAME=$1
    shift 1
    if "$@" 1>$TEST_NAME.stdout 2>$TEST_NAME.stderr; then
        ok $TEST_NAME
    else
        fail $TEST_NAME
        SUITE_RUN_FAILS=true
    fi
    SUITE_LOG_DIR=$(cylc get-global-config --print-run-dir)/$SUITE_NAME/log/suite
    mkdir -p $SUITE_LOG_DIR # directory might not exist if run fails very early
    cp $TEST_NAME.stdout $SUITE_LOG_DIR/out
    cp $TEST_NAME.stderr $SUITE_LOG_DIR/err
}

function suite_run_fail() {
    local TEST_NAME=$1
    shift 1
    if "$@" 1>$TEST_NAME.stdout 2>$TEST_NAME.stderr; then
        fail $TEST_NAME
        SUITE_RUN_FAILS=true
    else
        ok $TEST_NAME
    fi
    SUITE_LOG_DIR=$(cylc get-global-config --print-run-dir)/$SUITE_NAME/log/suite
    mkdir -p $SUITE_LOG_DIR
    cp $TEST_NAME.stdout $SUITE_LOG_DIR/out
    cp $TEST_NAME.stderr $SUITE_LOG_DIR/err
}

function cmp_ok() {
    local FILE_TEST=$1
    local FILE_CONTROL=${2:--}
    local TEST_NAME=$(basename $FILE_TEST)-cmp-ok
    local DIFF_CMD=${CYLC_TEST_DIFF_CMD:-'diff -u'}
    if ${DIFF_CMD} "$FILE_TEST" "$FILE_CONTROL" >"${TEST_NAME}.stderr" 2>&1;then
        ok $TEST_NAME
        return
    else
        cat "${TEST_NAME}.stderr" >&2
    fi
    mkdir -p $TEST_LOG_DIR
    cp $TEST_NAME.stderr $TEST_LOG_DIR/$TEST_NAME.stderr
    fail $TEST_NAME
}

function contains_ok() {
    local FILE_TEST=$1
    local FILE_CONTROL=${2:--}
    local TEST_NAME=$(basename $FILE_TEST)-contains-ok
    comm -13 <(sort "$FILE_TEST") <(sort "$FILE_CONTROL") \
        1>$TEST_NAME.stdout 2>$TEST_NAME.stderr
    if [[ -s $TEST_NAME.stdout ]]; then
        mkdir -p $TEST_LOG_DIR
        echo "Missing lines:" >>$TEST_NAME.stderr
        cat $TEST_NAME.stdout >>$TEST_NAME.stderr
        cp $TEST_NAME.stderr $TEST_LOG_DIR/$TEST_NAME.stderr
        fail $TEST_NAME
        return
    fi
    ok $TEST_NAME
}

function grep_ok() {
    local BRE=$1
    local FILE=$2
    local TEST_NAME=$(basename "$FILE")-grep-ok
    if grep -q -e "$BRE" "$FILE"; then
        ok $TEST_NAME
        return
    fi
    echo "Can't find $BRE in $FILE" > $TEST_NAME.stderr
    mkdir -p $TEST_LOG_DIR
    cp $TEST_NAME.stderr $TEST_LOG_DIR/$TEST_NAME.stderr
    fail $TEST_NAME
}

function greps_ok() {
    local IFS=$'\n'
    local BRES=("$1")
    local FILE=$2
    local TEST_NAME=$(basename "$FILE")-grep-ok
    for BRE in ${BRES[@]}
    do
        if ! grep -q -e "$BRE" "$FILE"
        then
            echo "Can't find $BRE in $FILE" > $TEST_NAME.stderr
            mkdir -p $TEST_LOG_DIR
            cp $TEST_NAME.stderr $TEST_LOG_DIR/$TEST_NAME.stderr
            fail $TEST_NAME
            return
        fi
    done
    ok $TEST_NAME
}

function count_ok() {
    local BRE=$1
    local FILE=$2
    local COUNT=$3
    local TEST_NAME=$(basename "$FILE")-count-ok
    NEW_COUNT=$(grep -c "$BRE" "$FILE")
    if (( NEW_COUNT == COUNT )); then
        ok $TEST_NAME
        return
    fi
    echo "Found $NEW_COUNT (not $COUNT) of $BRE in $FILE" > $TEST_NAME.stderr
    mkdir -p $TEST_LOG_DIR
    cp $TEST_NAME.stderr $TEST_LOG_DIR/$TEST_NAME.stderr
    fail $TEST_NAME
}

function exists_ok() {
    local FILE=$1
    local TEST_NAME=$(basename "$FILE")-file-exists-ok
    if [[ -a $FILE ]]; then
        ok $TEST_NAME
        return
    fi
    echo "Does not exist: $FILE" > $TEST_NAME.stderr
    mkdir -p $TEST_LOG_DIR
    cp $TEST_NAME.stderr $TEST_LOG_DIR/$TEST_NAME.stderr
    fail $TEST_NAME
}

function exists_fail() {
    local FILE=$1
    local TEST_NAME=$(basename $FILE)-file-exists-fail
    if [[ ! -a $FILE ]]; then
        ok $TEST_NAME
        return
    fi
    echo "Exists: $FILE" > $TEST_NAME.stderr
    mkdir -p $TEST_LOG_DIR
    cp $TEST_NAME.stderr $TEST_LOG_DIR/$TEST_NAME.stderr
    fail $TEST_NAME
}

function graph_suite() {
    # Generate a graphviz "plain" format graph of a suite.
    local SUITE_NAME="${1}"
    local OUTPUT_FILE="${2}"
    shift 2
    cylc graph --reference "${SUITE_NAME}" "$@" >"${OUTPUT_FILE}" 2>/dev/null
}

function init_suite() {
    local TEST_NAME=$1
    local SUITE_RC=${2:-'-'}
    local SNAME=$(echo ${TEST_SOURCE_DIR##*tests/} | tr '/' '_')
    SUITE_NAME="$(date -u +%Y%m%dT%H%M%SZ)_cylc_test_${SNAME}_${TEST_NAME}"
    mkdir "$TEST_DIR/$SUITE_NAME/"
    cat "$SUITE_RC" >"$TEST_DIR/$SUITE_NAME/suite.rc"
    cylc unregister "$SUITE_NAME" 2>/dev/null
    cylc register "$SUITE_NAME $TEST_DIR/$SUITE_NAME" 2>/dev/null
    while ! cylc get-directory "$SUITE_NAME" 1>/dev/null 2>&1; do
        sleep 1
        cylc unregister "$SUITE_NAME" 2>/dev/null
        cylc register "$SUITE_NAME" "$TEST_DIR/$SUITE_NAME" 2>/dev/null
    done
    cd "$TEST_DIR/$SUITE_NAME"
}

function install_suite() {
    ORIG_SUITE_NAME=$2
    SNAME=$( echo ${TEST_SOURCE_DIR##*tests/} | tr '/' '_' )
    SUITE_NAME=$(date -u +%Y%m%dT%H%M%SZ)_cylc_test_${SNAME}_${1}
    mkdir $TEST_DIR/$SUITE_NAME/
    cp -r $TEST_SOURCE_DIR/${2}/* $TEST_DIR/$SUITE_NAME
    cylc unregister $SUITE_NAME 2>/dev/null
    cylc register $SUITE_NAME $TEST_DIR/$SUITE_NAME 2>/dev/null
    while ! cylc get-directory $SUITE_NAME 1>/dev/null 2>&1; do
        sleep 1
        cylc unregister $SUITE_NAME 2>/dev/null
        cylc register $SUITE_NAME $TEST_DIR/$SUITE_NAME 2>/dev/null
    done
    cd $TEST_DIR/$SUITE_NAME
}

function purge_suite() {
    local SUITE_NAME="$1"
    if (($FAILURES == 0)); then
        local SUITE_DIR="$(cylc get-global-config --print-run-dir)/${SUITE_NAME}"
        while lsof | grep -q "${SUITE_DIR}"; do
            sleep 1
        done
        rm -fr "${SUITE_DIR}"
    fi
    cd "${TEST_DIR:-}"
    if [[ -n "${SUITE_NAME:-}" ]]; then
        cylc unregister "${SUITE_NAME}" 2>/dev/null || true
        if [[ -n "${TEST_DIR:-}" ]]; then
            rm -rf "${TEST_DIR}/${SUITE_NAME}/"
        fi
    fi
}

function poll() {
    local TIMEOUT=$(($(date +%s) + 60)) # wait 1 minute
    while (($(date +%s) < TIMEOUT)) && eval "$@"; do
        sleep 1
    done
}

function skip() {
    local N_TO_SKIP=$1
    shift 1
    local COUNT=0
    while ((COUNT++ < N_TO_SKIP)); do
        echo "ok $((++TEST_NUMBER)) # skip $@"
    done
}

function skip_all() {
    echo "1..0 # SKIP $@"
    exit
}

function ssh_install_cylc() {
    local RHOST="${1}"
    local RHOST_CYLC_DIR=$(_ssh_mkdtemp_cylc_dir "${RHOST}")
    TEST_RHOST_CYLC_DIR="${RHOST}:${RHOST_CYLC_DIR}"
    rsync -a '--exclude=*.pyc' "${CYLC_DIR}"/* "${RHOST}:${RHOST_CYLC_DIR}/"
}

function _ssh_mkdtemp_cylc_dir() {
    local RHOST="${1}"
    ssh -oBatchMode=yes -oConnectTimeout=5 "${RHOST}" python - <<'__PYTHON__'
import os
from tempfile import mkdtemp
print mkdtemp(dir=os.path.expanduser("~"), prefix="cylc-")
__PYTHON__
}

function mock_smtpd_init() {  # Logic borrowed from Rose
    local SMTPD_PORT=
    for SMTPD_PORT in 8025 8125 8225 8325 8425 8525 8625 8725 8825 8925; do 
        local SMTPD_HOST="localhost:${SMTPD_PORT}"
        local SMTPD_LOG="${TEST_DIR}/smtpd.log"
        python -m 'smtpd' -c 'DebuggingServer' -d -n "${SMTPD_HOST}" \
            1>"${SMTPD_LOG}" 2>&1 &
        local SMTPD_PID="$!"
        while ! grep -q 'DebuggingServer started' "${SMTPD_LOG}" 2>/dev/null; do
            if ps "${SMTPD_PID}" 1>/dev/null 2>&1; then
                sleep 1
            else
                rm -f "${SMTPD_LOG}"
                unset SMTPD_HOST SMTPD_LOG SMTPD_PID
                break
            fi
        done
        if [[ -n "${SMTPD_PID:-}" ]]; then
            TEST_SMTPD_HOST="${SMTPD_HOST}"
            TEST_SMTPD_PID="${SMTPD_PID}"
            TEST_SMTPD_LOG="${SMTPD_LOG}"
            break
        fi
    done
}

function mock_smtpd_kill() {  # Logic borrowed from Rose
    if [[ -n "${TEST_SMTPD_PID:-}" ]] && ps "${TEST_SMTPD_PID}" >/dev/null 2>&1
    then
        kill "${TEST_SMTPD_PID}"
        wait "${TEST_SMTPD_PID}" 2>/dev/null || true
        unset TEST_SMTPD_HOST TEST_SMTPD_LOG TEST_SMTPD_PID
    fi
}

function create_test_globalrc() {
    # (Documentated in file header).
    PRE=""
    POST=""
    if (( $# == 1 )); then
        PRE=$1
    elif (( $# == 2 )); then
        PRE=$1
        POST=$2
    elif (( $# > 2 )); then
        >&2 echo "ERROR, create_test_globalrc: too many args"
        exit 1
    fi
    # Tidy in case of previous use of this function.
    rm -rf conf
    mkdir conf
    # Suite host self-identification method.
    echo "$PRE" > conf/global.rc
    TESTS_CONF_FILE="$(cylc get-global --print-site-dir)/global-tests.rc"
    [[ -f "$TESTS_CONF_FILE" ]] && cat "$TESTS_CONF_FILE" >> conf/global.rc
    echo "$POST" >> conf/global.rc
    export CYLC_CONF_PATH=$PWD/conf
}

CYLC_DIR=${CYLC_DIR:-$(cd $(dirname $BASH_SOURCE)/../../.. && pwd)}
PATH=$CYLC_DIR/bin:$PATH

TEST_NAME_BASE=$(basename $0 .t)
TEST_SOURCE_DIR=$(cd $(dirname $0) && pwd)
TEST_DIR=$(mktemp -d)
cd $TEST_DIR
TEST_LOG_DIR_BASE=${TMPDIR:-/tmp}/cylc-tests-"$LOGNAME"/$(basename $TEST_SOURCE_DIR)
TEST_LOG_DIR=$TEST_LOG_DIR_BASE/$TEST_NAME_BASE-$(date -u +%Y%m%dT%H%M%SZ)
SUITE_RUN_FAILS=false
SSH_OPTS='-oBatchMode=yes -oConnectTimeout=10'

# these variables should be moved to site/user config:
export CYLC_TEST_TASK_HOST=${CYLC_TEST_TASK_HOST:-localhost}
export CYLC_TEST_TASK_OWNER=${CYLC_TEST_TASK_OWNER:-$USER}

CYLC_TEST_SKIP=${CYLC_TEST_SKIP:-}
# Is this test in the skip list?
THIS=${0#./}
THIS_DIR=$(dirname $THIS)
for SKIP in ${CYLC_TEST_SKIP}; do
    RSKIP=${SKIP#./}
    if [[ ${THIS} == ${RSKIP} || ${THIS_DIR%/} == ${RSKIP%/} ]]; then
        skip_all "this test is in \$CYLC_TEST_SKIP."
        break
    fi
done
if ! $CYLC_TEST_RUN_GENERIC && $CYLC_TEST_IS_GENERIC; then
    skip_all "not running generic tests"
elif ! $CYLC_TEST_RUN_PLATFORM && ! $CYLC_TEST_IS_GENERIC; then
    skip_all "not running platform-specific tests"
fi
set +x

set +e
