#!/bin/sh
#
#  Copyright (C) CFEngine AS
#
#  This file is part of CFEngine 3 - written and maintained by CFEngine AS.
#
#  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; version 3.
#
#  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, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
#
# To the extent this program is licensed as part of the Enterprise
# versions of CFEngine, the applicable Commercial Open Source License
# (COSL) may apply to this file if you as a licensee so wish it. See
# included file COSL.txt.
#

#
# Detect and replace non-POSIX shell
#
try_exec() {
  type "$1" > /dev/null 2>&1 && exec "$@"
}

unset foo
(: ${foo%%bar}) 2> /dev/null
T1="$?"

if test "$T1" != 0; then
  try_exec /usr/xpg4/bin/sh "$0" "$@"
  echo "No compatible shell script interpreter found."
  echo "Please find a POSIX shell for your system."
  exit 42
fi

#
# Explicitly use POSIX tools if needed
#
if [ -f /usr/xpg4/bin/grep ]
then
    PATH=/usr/xpg4/bin:$PATH
    export PATH
fi

#
# Unset environment variables which might break runinng acceptance tests
#
GREP_OPTIONS=
export GREP_OPTIONS

#
# Defaults (overridden by command-line arguments)
#
LOG=test.log
INOTIFYWATCH_LOG=inotifywatch.log
INOTIFYWAIT_LOG=inotifywait.log
INOTIFYWATCH_OPTS="-r -e modify -e attrib -e moved_to -e create -e delete -e delete_self"
INOTIFYWAIT_OPTS="-r -m -e modify -e attrib -e moved_to -e create -e delete -e delete_self"
SUMMARY=summary.log
XML=test.xml
WHOAMI=/c/Windows/System32/whoami.exe
BASE_WORKDIR="$(pwd)/workdir"
QUIET=
TIMED_TESTS=${TIMED_TESTS:-1}
export TIMED_TESTS
CRASHING_TESTS=${CRASHING_TESTS:-1}
export CRASHING_TESTS
STAGING_TESTS=${STAGING_TESTS:-0}
export STAGING_TESTS
NETWORK_TESTS=${NETWORK_TESTS:-1}
export NETWORK_TESTS
UNSAFE_TESTS=${UNSAFE_TESTS:-0}
export UNSAFE_TESTS
LIBXML2_TESTS=${LIBXML2_TESTS:-1}
export LIBXML2_TESTS
BINDIR=${BINDIR:-}
export BINDIR
NO_CLEAN=${NO_CLEAN:-0}
export NO_CLEAN
BASECLASSES=${BASECLASSES:-AUTO}
export BASECLASSES
EXTRACLASSES=${EXTRACLASSES:-DEBUG}
export EXTRACLASSES
VALGRIND_OPTS="${VALGRIND_OPTS:---leak-check=full --show-reachable=yes --suppressions=valgrind-suppressions}"
export VALGRIND_OPTS
AGENT=${AGENT:-}
export AGENT
CF_PROMISES=${CF_PROMISES:-}
export CF_PROMISES
CF_SERVERD=${CF_SERVERD:-}
export CF_SERVERD
CF_KEY=${CF_KEY:-}
export CF_KEY
RPMVERCMP=${RPMVERCMP:-}
export RPMVERCMP
LIBTOOL=${LIBTOOL:-}
export LIBTOOL
INCLUDE_IN_WORKDIR=${INCLUDE_IN_WORKDIR:-}
export INCLUDE_IN_WORKDIR

export MAKEFLAGS
export GAINROOT

# Use TEST_INDEX for indexing a poor man's array. Basically the subscript is
# just appended to the variable name.
TEST_INDEX=0
TEST_TIMED_INDEX=0
#TESTS_TIMED_<index>=0
#TESTS_TIMEOUT_<index>=0
#TESTS_PASSES_<index>=0
TESTS_COUNT=0
TESTS_NORMAL_COUNT=0
TESTS_TIMED_COUNT=0
TESTS_TIMED_REMAINING=0

case "$OSTYPE" in
    msys)
        if "$WHOAMI" -priv | grep SeTakeOwnershipPrivilege > /dev/null
        then
            # Don't use elevate if we already have the privileges. It is slower and
            # pops up a flashing window for every single test.
            DEFAULT_GAINROOT=
        else
            DEFAULT_GAINROOT="`dirname $0`/tool_wrappers/elevate.sh"
        fi
        # Use hardlinks on Windows. Using symbolic links will work, but Msys creates a
        # real copy, which eats disk space very quickly when you multiply with the number
        # of tests.
        LN_CMD="ln -f"
        ;;
    *)
        DEFAULT_GAINROOT=fakeroot
        LN_CMD="ln -sf"
        ;;
esac

GAINROOT=${GAINROOT:-$DEFAULT_GAINROOT}

PASSED_TESTS=0
FAILED_TESTS=0
SUPPRESSED_FAILURES=0
SOFT_FAILURES=0
SKIPPED_TESTS=0

#
# Many older platforms don't support date +%s, so check for compatibility
# and find Perl for the unix_seconds() routine below. (Mantis #1254)
#
HAVE_DATE_PCT_S=
date +%s | grep %s >/dev/null 2>&1
if [ $? -ne 0 ]
then
    HAVE_DATE_PCT_S=1
fi
PERL=`which perl 2>/dev/null`

# color!
if [ "${CFENGINE_COLOR}" = "1" ]
then
    COLOR_SUCCESS="\\033[1;32m"
    COLOR_FAILURE="\\033[1;31m"
    COLOR_WARNING="\\033[1;33m"
    COLOR_NORMAL="\\033[0;39m"
else
    COLOR_SUCCESS=
    COLOR_FAILURE=
    COLOR_WARNING=
    COLOR_NORMAL=
fi

#
# Obtain UNIX time(), using date +%s, Perl, or POSIX-compatible approach.
#
unix_seconds() {
    if [ "$HAVE_DATE_PCT_S" ]
    then
        date +%s
        return 0
    fi

    if [ "$PERL" ]
    then
        $PERL -e 'print time() . "\n"' 2>/dev/null
        if [ $? -eq 0 ]
        then
            return 0
        fi
    fi

    # Last resort if Perl fails - the extended cpio interchange format has
    # the file modification timestamp in columns 48-59, in octal.
    : > $BASE_WORKDIR/x
    echo "ibase=8;$(pax -wx cpio $BASE_WORKDIR/$$.seconds | cut -c 48-59)" | bc 2>/dev/null
    rm $BASE_WORKDIR/x
}

usage() {
    echo "testall [-h|--help] [-q|--quiet] [--gainroot=<command>] [--agent=<agent>] [--cfpromises=<cf-promises>] [--cfserverd=<cf-serverd>] [--cfkey=<cf-key>] [--bindir=<bindir>] [--staging] [--unsafe] [--no-network] [--gdb] [--printlog] [<test> <test>...]"
    echo
    echo "If no test is given, all standard tests are run:"
    echo "  Tests with names of form <file>.cf are expected to run successfully"
    echo "  Tests with names of form <file>.x.cf are expected to crash"
    echo "Set ${COLOR_SUCCESS}CFENGINE_COLOR=1${COLOR_NORMAL} to get ANSI color markers where appropriate."
    echo
    echo "If arguments are given, those are executed as tests"
    echo
    echo " -h"
    echo " --help    prints usage"
    echo " -q"
    echo " --quiet   makes script much quieter"
    echo " --gainroot=<command>  forces use of command to gain root privileges,"
    echo "           otherwise fakeroot is used.  Use --gainroot=env to make this"
    echo "           option a no-op (e.g. you're running inside fakeroot already)"
    echo " --agent   provides a way to specify non-default cf-agent location,"
    echo "           and defaults to $DEFAGENT."
    echo " --baseclasses  provides a way to override the default cf-agent classes,"
    echo "           and defaults to ${BASECLASSES}.  Also can use --bc"
    echo " --extraclasses  provides a way to append to the default cf-agent classes,"
    echo "           and defaults to ${EXTRACLASSES}.  Also can use --ec"
    echo " --cfpromises  provides a way to specify non-default cf-promises location,"
    echo "           and defaults to $DEFCF_PROMISES."
    echo " --cfserverd  provides a way to specify non-default cf-serverd location,"
    echo "           and defaults to $DEFCF_SERVERD."
    echo " --cfkey   provides a way to specify non-default cf-key location,"
    echo "           and defaults to $DEFCF_KEY."
    echo " --rpmvercmp  provides a way to specify non-default rpmvercmp location,"
    echo "           and defaults to $DEFRPMVERCMP."
    echo " --bindir  specifies the directory containing all the binaries."
    echo "           Mutually exclusive with --agent and --cf* arguments."
    echo " --libtool specify non-default libtool location (only needed for --gdb)."
    echo "               defaults to $DEFLIBTOOL."
    echo " --include Include the file or directory given in the workdir before the test"
    echo "           starts. This option may be given several times."
    echo " --staging enable tests in staging directories. They are not expected to pass."
    echo " --unsafe  enable tests in unsafe directories. WARNING! These tests modify the"
    echo "           system they're running on and can DAMAGE YOUR SYSTEM! DO NOT use"
    echo "           this option without a backup."
    echo "           If you use this option you should also use --gainroot=sudo,"
    echo "           otherwise you will get incorrect results."
    echo " --no-network disable tests in network directories."
    echo " --no-libxml2 disable tests involving xml file editing."
    echo " --no-crashing disable tests that are expected to crash (for use with valgrind)."
    echo " --no-timed disable timed tests that may introduce wait times."
    echo " --printlog   print the full test.log output immediately.  Override with $PRINTLOG"
    echo " --gdb          Run test under GDB"
    echo " --valgrind     Run test under Valgrind"
    echo " --callgrind    Run test under valgrind --tool=callgrind"
    echo " --inotifywatch Run tests and log filesystem statistics"
    echo " --inotifywait  Run tests and log filesystem events"
    echo " --no-clean does not clean workdir after test finishes"
    echo "            (by default it gets cleaned only if test passed)."
    echo " -j[n]"
    echo " -jobs=[n] Run tests in parallel, works like make -j option. Note that some"
    echo "           tests will always run one by one."
}

workdir() {
    echo "$BASE_WORKDIR/$(echo "$1" | sed 's,[./],_,g')"
}

# Takes the following arguments:
# 1. Agent - The agent to execute.
# 2. Test - The test to execute.
# 3. Pass number - [Optional] The current pass number. Used by timed tests.
# 4. Timeout variable - [Optional] The name of the variable to put the next
#        timeout into. Used by timed tests.
runtest() {
    # Clear local variables
    unset AGENT TEST EXPECTED_CRASH SKIP SKIPREASON RESULT RESULT_MSG TEST_START_TIME FLATNAME WORKDIR OUTFILE TEST_DESCRIPTION TEST_STORY TEST_COVERS

    AGENT="$1"
    TEST="$2"
    PASS_NUM="$3"
    NEXT_TIMEOUT_VAR="$4"
    # With STAY_IN_WORKDIR we may be running alongside others, and need to print everything at once
    # on one line, so that we don't risk mixing lines together.
    if [ -z "$QUIET" -a -z "$STAY_IN_WORKDIR" ]
    then
        printf "$TEST "
    fi

    if echo "$TEST" | fgrep -e .x.cf > /dev/null
    then
        EXPECTED_CRASH=1
    else
        EXPECTED_CRASH=
    fi

    if [ "x$CRASHING_TESTS" = "x0" ] && [ "x$EXPECTED_CRASH" = "x1" ]
    then
        SKIP=1
        SKIPREASON="${COLOR_WARNING}Crashing tests are disabled${COLOR_NORMAL}"
    elif [ "x$STAGING_TESTS" = "x0" ] && echo "$TEST" | grep '/staging/' > /dev/null
    then
        SKIP=1
        SKIPREASON="${COLOR_WARNING}Staging tests are disabled${COLOR_NORMAL}"
    elif [ "x$UNSAFE_TESTS" != "x1" ] && echo "$TEST" | grep '/unsafe/' > /dev/null
    then
        SKIP=1
        SKIPREASON="${COLOR_WARNING}Unsafe tests are disabled${COLOR_NORMAL}"
    elif [ "x$NETWORK_TESTS" = "x0" ] && echo "$TEST" | grep '/network/' > /dev/null
    then
        SKIP=1
        SKIPREASON="${COLOR_WARNING}Network-dependent tests are disabled${COLOR_NORMAL}"
    elif [ "x$LIBXML2_TESTS" = "x0" ] && echo "$TEST" | grep '/11_xml_edits/' > /dev/null
    then
        SKIP=1
        SKIPREASON="XML file editing tests are disabled"
    else
        SKIP=
        SKIPREASON=
    fi

    TEST_START_TIME=$(unix_seconds)

    # Create workdir
    WORKDIR="$(workdir "$TEST")"
    OUTFILE="$WORKDIR/output.log"
    rm -f "$OUTFILE"
    if [ -z "$PASS_NUM" ] || [ "$PASS_NUM" -eq 1 ]
    then
        # Don't reset workdir if this is a subsequent pass.
        $GAINROOT rm -rf "$WORKDIR"
        mkdir -p "$WORKDIR/bin" "$WORKDIR/tmp"
        chmod ugo+rwxt "$WORKDIR/tmp"
    fi

    if [ -n "$NEXT_TIMEOUT_VAR" ]
    then
        eval $NEXT_TIMEOUT_VAR=
    fi

    if [ -n "$SKIP" ]
    then
        TEST_END_TIME=$TEST_START_TIME
        RESULT=Skip
        RESULT_MSG="${COLOR_WARNING}Skipped ($SKIPREASON)${COLOR_NORMAL}"
    else

        # Prepare workdir
        if [ -z "$PASS_NUM" ] || [ "$PASS_NUM" -eq 1 ]
        then
            # Don't copy into workdir if this is a subsequent pass.
            if [ -n "$BINDIR" ]
            then
                # Copy everything, because Windows depends on DLLs.
                $LN_CMD "$BINDIR"/* "$WORKDIR/bin"
            else
                $LN_CMD "$AGENT" "$WORKDIR/bin"
                $LN_CMD "$CF_PROMISES" "$WORKDIR/bin"
                $LN_CMD "$CF_SERVERD" "$WORKDIR/bin"
                $LN_CMD "$CF_KEY" "$WORKDIR/bin"
                $LN_CMD "$RPMVERCMP" "$WORKDIR/bin"
            fi
            for inc in $INCLUDE_IN_WORKDIR; do (
                # Copy directory structure, but make links inside. This allows tests to
                # add additional files to the directory.
                base=$(basename $inc)
                mkdir -p "$WORKDIR/$base"
                cd $inc || exit 2
                for dir in $(find . -type d)
                do
                    mkdir -p "$WORKDIR/$base/$dir"
                done
                for file in $(find . \! -type d)
                do
                    $LN_CMD "$inc/$file" "$WORKDIR/$base/$file"
                done
            ); done
        fi
        if uname | grep MINGW > /dev/null
        then
            PLATFORM_WORKDIR="$(echo $WORKDIR | sed -e 's%^/\([a-cA-Z]\)/%\1:/%' | sed -e 's%/%\\%g')"
            DS="\\"
        else
            PLATFORM_WORKDIR="$WORKDIR"
            DS="/"
        fi

        ( echo ----------------------------------------------------------------------
            echo "$TEST"${EXPECTED_CRASH:+ \(expected to crash\)}${SKIPREASON:+ \($SKIPREASON\)}
            echo ----------------------------------------------------------------------
        ) >> "$WORKDIR/$LOG"

        echo "#!/bin/sh
CFENGINE_TEST_OVERRIDE_WORKDIR=\"$PLATFORM_WORKDIR\"
TEMP=\"$PLATFORM_WORKDIR${DS}tmp\"
CFENGINE_TEST_OVERRIDE_EXTENSION_LIBRARY_DIR=\"$CFENGINE_TEST_OVERRIDE_EXTENSION_LIBRARY_DIR\"
export CFENGINE_TEST_OVERRIDE_WORKDIR TEMP CFENGINE_TEST_OVERRIDE_EXTENSION_LIBRARY_DIR

" > "$WORKDIR/runtest"

        if [ "$GDB" = 1 ]
        then
            if grep libtool < "$AGENT" > /dev/null
            then
                printf "\"$LIBTOOL\" --mode=execute " >> "$WORKDIR/runtest"
            fi
            printf "gdb --args " >> "$WORKDIR/runtest"
        fi

        if [ -n "$USE_VALGRIND" ] && [ x"$EXPECTED_CRASH" = "x" ]
        then
            if grep libtool < "$AGENT" > /dev/null
            then
                printf "\"$LIBTOOL\" --mode=execute " >> "$WORKDIR/runtest"
            fi
            printf "valgrind ${VALGRIND_OPTS} \"$AGENT\" $VERBOSE -Kf \"$TEST\" -D ${PASS_NUM:+test_pass_$PASS_NUM,}${BASECLASSES},${EXTRACLASSES} 2>&1\n" >> "$WORKDIR/runtest"
        else
            printf "\"$AGENT\" $VERBOSE -Kf \"$TEST\" -D ${PASS_NUM:+test_pass_$PASS_NUM,}${BASECLASSES},${EXTRACLASSES}\n" >> "$WORKDIR/runtest"
        fi

        chmod +x "$WORKDIR/runtest"

        if [ "$GDB" = 1 ]
        then
            $GAINROOT "$WORKDIR/runtest"
        else
            eval $GAINROOT "$WORKDIR/runtest" >>$OUTFILE 2>&1
        fi
        RETVAL=$?
        cat $OUTFILE >> "$WORKDIR/$LOG"
        echo >> "$WORKDIR/$LOG"
        echo "Return code is $RETVAL." >> "$WORKDIR/$LOG"

        TEST_END_TIME=$(unix_seconds)

        # Try to collect test metadata if any
        if egrep "R: test description: " $OUTFILE > /dev/null
        then
            TEST_DESCRIPTION="$(egrep "R: test description" $OUTFILE | sed -e "s,.*test description: \([A-Za-z0-9_]*\),\1,")"
        fi
        if egrep -e "R: test story_id: " $OUTFILE > /dev/null
        then
            TEST_STORY="$(egrep "R: test story_id" $OUTFILE | sed -e "s,.*test story_id: \([0-9][0-9]*\),\1,")"
        fi
        if egrep -e "R: test covers: " $OUTFILE > /dev/null
        then
            TEST_COVERS="$(egrep "R: test covers" $OUTFILE | sed -e "s,.*test covers: \([A-Za-z0-9_]*\),\1,")"
        fi


        RESULT_MSG=
        if [ -z "$EXPECTED_CRASH" ]
        then
            # We need to be careful when matching test outcomes. Because of convergence
            # passes, a test may output FAIL before it outputs Pass; in this case the
            # latter trumps the former. Also be careful when matching an [XS]FAIL; the
            # test should not be allowed to output any other outcome in the same run,
            # except for FAIL.

            # Some states are output by dcs.cf.sub, therefore check for both TEST
            # prefix and dcs.cf.sub prefix.
            ESCAPED_TEST="$(echo "($TEST|dcs.cf.sub)" | sed -e 's/\./\\./g')"
            if egrep "R: .*$ESCAPED_TEST [XS]FAIL" $OUTFILE > /dev/null && ! egrep "R: .*$ESCAPED_TEST Wait/" $OUTFILE > /dev/null
            then
                # Check for other test case outcomes than fail. Should not happen.
                if egrep "R: .*$ESCAPED_TEST " $OUTFILE | egrep -v "R: .*$ESCAPED_TEST [XS]?FAIL" > /dev/null
                then
                    RESULT=FAIL
                    RESULT_MSG="${COLOR_FAILURE}FAIL (The test Passed, but failure was expected)${COLOR_NORMAL}"
                else
                    TICKET="$(egrep "R: .*$ESCAPED_TEST [XS]FAIL" $OUTFILE | sed -e "s,.*[XS]FAIL/\(.*\),\1,")"
                    RESULT="$(egrep "R: .*$ESCAPED_TEST [XS]FAIL" $OUTFILE | sed -e "s,.*\([XS]FAIL\).*,\1,")"
                    if [ "$RESULT" = "XFAIL" ]
                    then
                        RESULT_MSG="${COLOR_WARNING}FAIL (Suppressed, $TICKET)${COLOR_NORMAL}"
                    else
                        RESULT_MSG="${COLOR_WARNING}Soft fail ($TICKET)${COLOR_NORMAL}"
                    fi
                fi
            elif [ $RETVAL -ne 0 ]
            then
                RESULT=FAIL
            elif egrep "R: .*$ESCAPED_TEST FAIL/no_ticket_number" $OUTFILE > /dev/null
            then
                RESULT=FAIL
                RESULT_MSG="${COLOR_FAILURE}FAIL (Tried to suppress failure, but no issue number is provided)${COLOR_NORMAL}"
            elif egrep "R: .*$ESCAPED_TEST Wait/[0-9]+" $OUTFILE > /dev/null
            then
                if [ -z "$NEXT_TIMEOUT_VAR" ]
                then
                    RESULT=FAIL
                    RESULT_MSG="${COLOR_FAILURE}FAIL (Test tried to wait but is not in \"timed\" directory)${COLOR_NORMAL}"
                else
                    WAIT_TIME=$(egrep "R: .*$ESCAPED_TEST Wait/[0-9]+" $OUTFILE | sed -e 's,.*Wait/\([0-9][0-9]*\).*,\1,')
                    eval $NEXT_TIMEOUT_VAR=$(($TEST_END_TIME+$WAIT_TIME))
                    RESULT=Wait
                    RESULT_MSG="Awaiting ($WAIT_TIME seconds)..."
                fi
            elif egrep "R: .*$ESCAPED_TEST Pass" $OUTFILE > /dev/null
            then
                RESULT=Pass
                RESULT_MSG="${COLOR_SUCCESS}Pass${COLOR_NORMAL}"
            elif egrep "R: .*$ESCAPED_TEST Skip/unsupported" $OUTFILE > /dev/null
            then
                RESULT=Skip
                RESULT_MSG="${COLOR_WARNING}Skipped (No platform support)${COLOR_NORMAL}"
            elif egrep "R: .*$ESCAPED_TEST Skip/needs_work" $OUTFILE > /dev/null
            then
                RESULT=Skip
                RESULT_MSG="${COLOR_WARNING}Skipped (Test needs work)${COLOR_NORMAL}"
            else
                RESULT=FAIL
                RESULT_MSG="${COLOR_FAILURE}FAIL${COLOR_NORMAL}"
            fi
        else
            if [ $RETVAL -gt 0 ] && [ $RETVAL -lt 128 ]
            then
                RESULT=Pass
                RESULT_MSG="${COLOR_SUCCESS}Pass${COLOR_NORMAL}"
            elif [ $RETVAL -ge 128 ]
            then
                RESULT=FAIL
                RESULT_MSG="${COLOR_FAILURE}FAIL (Crashed)${COLOR_NORMAL}"
            else
                RESULT=FAIL
                RESULT_MSG="${COLOR_FAILURE}FAIL (Failed to exit with non-zero status)${COLOR_NORMAL}"
            fi
        fi

        if [ "x$RESULT_MSG" = "x" ]
        then
            RESULT_MSG=$RESULT
        fi

        if [ "$RESULT" = "FAIL" ] && [ -e .succeeded/"$FLATNAME" ]
        then
            RESULT_MSG="${COLOR_FAILURE}$RESULT_MSG (UNEXPECTED FAILURE)${COLOR_NORMAL}"
        fi
    fi


    if [ "$RESULT" = "XFAIL" -o "$RESULT" = "SFAIL" ]
    then
        echo "    <testcase name=\"$(basename $TEST)\""
        echo "              classname=\"$TEST $RESULT_MSG\""
        echo "              time=\"$(($TEST_END_TIME - $TEST_START_TIME)) seconds\">"
    elif [ "$RESULT" != Wait ]
    then
        echo "    <testcase name=\"$(basename $TEST)\""
        echo "              classname=\"$TEST\""
        echo "              time=\"$(($TEST_END_TIME - $TEST_START_TIME)) seconds\">"
    fi >> "$WORKDIR/$XML"

    
    # Fill test metadata if any
    if [ ! -z "$TEST_DESCRIPTION" ] || [ ! -z "$TEST_STORY" ] || [ ! -z "$TEST_COVERS" ]
    then
        if [ ! -z "$TEST_DESCRIPTION" ]
        then
            TEST_DESCRIPTION_METATAG="name=\"$TEST_DESCRIPTION\""
        fi
        if [ ! -z "$TEST_STORY" ]
        then
            TEST_STORY_METATAG="story_id=\"$TEST_STORY\""
        fi
        if [ ! -z "$TEST_COVERS" ]
        then
            TEST_COVERS_METATAG="covers=\"$TEST_COVERS\""
        fi
        echo "        <system-out>$TEST_DESCRIPTION_METATAG $TEST_STORY_METATAG $TEST_COVERS_METATAG</system-out>"
    fi >> "$WORKDIR/$XML"


    echo $RESULT $TEST >> "$WORKDIR/$SUMMARY"
    case "$RESULT" in
        Pass)
            PASSED_TESTS=$(($PASSED_TESTS + 1))

            mkdir -p '.succeeded'
            touch .succeeded/"$FLATNAME"
            ;;

        FAIL|XFAIL)
            ( echo "        <failure type=\"$RESULT\""
                echo "                 message=\"$RESULT_MSG $TEST\">"
                cat $OUTFILE | sed -e "s/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/'/\&quot;/g"
                echo "        </failure>"
            ) >> "$WORKDIR/$XML"
            FAILED_TESTS=$(($FAILED_TESTS + 1))
            if [ "$RESULT" = "XFAIL" ]
            then
                SUPPRESSED_FAILURES=$(($SUPPRESSED_FAILURES + 1))
            fi
            ;;

        SFAIL)
            ( echo "        <skipped type=\"$RESULT_MSG\">"
                echo "                 message=\"$RESULT_MSG $TEST\">"
                cat $OUTFILE | sed -e "s/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/'/\&quot;/g"
                echo "        </skipped>"
            ) >> "$WORKDIR/$XML"
            SOFT_FAILURES=$(($SOFT_FAILURES + 1))
            ;;

        Skip)
            ( echo "        <skipped type=\"$RESULT_MSG\">"
                echo "        </skipped>"
            ) >> "$WORKDIR/$XML"
            SKIPPED_TESTS=$(($SKIPPED_TESTS + 1))
            ;;
    esac

    if [ "$RESULT" != Wait ]
    then
        echo "    </testcase>" >> "$WORKDIR/$XML"
    fi

    if [ -e "$WORKDIR" -a "$RESULT" != "FAIL" -a "$RESULT" != "Wait" -a "$NO_CLEAN" = "0" ]
    then
        # Delete everything except the logs from the workdir.
        ls -1 "$WORKDIR" | while read s
        do
            case "$s" in
                "$LOG"|"$SUMMARY"|"$XML")
                    ;;
                *)
                    $GAINROOT rm -fr "$WORKDIR/$s"
                    ;;
            esac
        done
    fi

    if [ -z "$QUIET" ]
    then
        if [ -n "$STAY_IN_WORKDIR" ]
        then
            # See comment about STAY_IN_WORKDIR near start of runtest.
            echo "$TEST $RESULT_MSG"
        else
            echo $RESULT_MSG
        fi
    else
        if [ "$RESULT" = Pass ]
        then
            printf '.'
        elif [ "$RESULT" = Skip ]
        then
            printf '-'
        elif [ "$RESULT" = FAIL ]
        then
            if [ -n "$EXPECTED_CRASH" ]
            then
                printf '!'
            else
                printf 'x'
            fi
        fi
    fi

    (
        echo
        echo '  ==>' "$RESULT_MSG"
        echo
    ) >> "$WORKDIR/$LOG"
}

ORIG_ARGS=
while true
do
    case "$1" in
        -h|--help)
            usage
            exit;;
        -q|--quiet)
            QUIET=1;;
        --gainroot=*)
            GAINROOT=${1#--gainroot=};;
        --valgrind)
            USE_VALGRIND=1;;
        --callgrind)
            USE_VALGRIND=1
            VALGRIND_OPTS="--suppressions=valgrind-suppressions --tool=callgrind";;
        --inotifywatch)
            USE_INOTIFYWATCH=1;;
        --inotifywait)
            USE_INOTIFYWAIT=1;;
        --staging)
            STAGING_TESTS=1;;
        --unsafe)
            UNSAFE_TESTS=1;;
        --no-network)
            NETWORK_TESTS=0;;
        --no-libxml2)
            LIBXML2_TESTS=0;;
        --no-crashing)
            CRASHING_TESTS=0;;
        --no-timed)
            TIMED_TESTS=0;;
        --agent=*)
            AGENT=${1#--agent=};;
        --baseclasses=*)
            BASECLASSES=${1#--baseclasses=};;
        --bc=*)
            BASECLASSES=${1#--bc=};;
        --extraclasses=*)
            EXTRACLASSES=${1#--extraclasses=};;
        --ec=*)
            EXTRACLASSES=${1#--ec=};;
        --cfpromises=*)
            CF_PROMISES=${1#--cfpromises=};;
        --cfserverd=*)
            CF_SERVERD=${1#--cfserverd=};;
        --cfkey=*)
            CF_KEY=${1#--cfkey=};;
        --rpmvercmp=*)
            RPMVERCMP=${1#--rpmvercmp=};;
        --bindir=*)
            BINDIR=${1#--bindir=};;
        --libtool=*)
            LIBTOOL=${1#--libtool=};;
        --include=*)
            INCLUDE_IN_WORKDIR="$INCLUDE_IN_WORKDIR${INCLUDE_IN_WORKDIR:+ }${1#--include=}";;
        -j*|--jobs*)
            MAKEFLAGS="$MAKEFLAGS $1";;
        --printlog)
            PRINTLOG=1;;
        --gdb)
            GDB=1;;
        --no-clean)
            NO_CLEAN=1;;
        --stay-in-workdir)
            # Internal option. Meant to keep sub invocations from interfering by
            # writing files only into the workdir.
            STAY_IN_WORKDIR=1;;
        -*)
            echo "Unknown option: $1"
            exit 1;;
        *)
            break;;
    esac
    # Make sure spaces are preserved by escaping them.
    ORIG_ARGS="$ORIG_ARGS $(echo "$1" | sed -e 's/ /\\ /g')"
    shift
done

#
# Close stdin file descriptor to avoid tty_interactive check in cf-agent being success.
#
if [ "$GDB" != 1 ] || [ "$USE_VALGRIND" != 1 ]
then
    exec </dev/null
fi

if [ "$OSTYPE" = "msys" ] && [ "$TESTALL_DO_NOT_RECURSE" != 1 ] &&
    ! "$WHOAMI" -priv | grep SeTakeOwnershipPrivilege > /dev/null
    then
    # On Windows we run the entire test run under GAINROOT, because doing it for
    # each test is horribly slow.
    echo "export GAINROOT=" > runtests.sh
    echo "export TESTALL_DO_NOT_RECURSE=1" >> runtests.sh
    echo "export VERBOSE='$VERBOSE'" >> runtests.sh
    echo "$0 $ORIG_ARGS $@ &" >> runtests.sh
    # Note quote change. We want to keep below variables unexpanded.
    echo 'trap "kill $!" INT' >> runtests.sh
    echo 'trap "kill $!" TERM' >> runtests.sh
    # Traps do not fire during commands, but *do* fire during wait.
    echo 'wait $!' >> runtests.sh
    $GAINROOT "./runtests.sh"
    exit $?
fi

# Check last -j flag, and check if it is -j1.
for arg in $MAKEFLAGS
do
    case "$arg" in
        -j|--jobs)
            PARALLEL=1 ;;
        -j*)
            if [ 1 -eq "${arg#-j}" ]
            then
                PARALLEL=
            else
                PARALLEL=1
            fi
            ;;
        --jobs=*)
            if [ 1 -eq "${arg#--jobs=}" ]
            then
                PARALLEL=
            else
                PARALLEL=1
            fi
            ;;
    esac
done

if [ -n "$AGENT" -o -n "$CF_PROMISES" -o -n "$CF_SERVERD" -o -n "$CF_KEY" -o -n "$RPMVERCMP" ]
then
    if [ -n "$BINDIR" ]
    then
        echo "--bindir is mutually exclusive with specifying individual binaries."
        exit 2
    fi
fi

# We assume we're running this script from $objdir, $objdir/tests/acceptance,
# or /var/cfengine/tests/acceptance.
find_default_binary()
{
    [ -x "`pwd`/../../ext/$2" ] && eval $1=\""`pwd`/../../ext/$2"\"
    [ -x "`pwd`/../../bin/$2" ] && eval $1=\""`pwd`/../../bin/$2"\"
    [ -x "`pwd`/../../$2/$2" ] && eval $1=\""`pwd`/../../$2/$2"\"
    [ -x "`pwd`/$2/$2" ] && eval $1=\""`pwd`/$2/$2"\"
    [ -n "$BINDIR" -a -x "$BINDIR/$2" ] && eval $1=\""$BINDIR/$2"\"
}
find_default_binary DEFAGENT cf-agent
find_default_binary DEFCF_PROMISES cf-promises
find_default_binary DEFCF_SERVERD cf-serverd
find_default_binary DEFCF_KEY cf-key
find_default_binary DEFRPMVERCMP rpmvercmp

[ -x "`pwd`/libtool" ] && DEFLIBTOOL="`pwd`/libtool"
[ -x "`pwd`/../../libtool" ] && DEFLIBTOOL="`pwd`/../../libtool"

AGENT=${AGENT:-${DEFAGENT}}
CF_PROMISES=${CF_PROMISES:-${DEFCF_PROMISES}}
CF_SERVERD=${CF_SERVERD:-${DEFCF_SERVERD}}
CF_KEY=${CF_KEY:-${DEFCF_KEY}}
RPMVERCMP=${RPMVERCMP:-${DEFRPMVERCMP}}
LIBTOOL=${LIBTOOL:-${DEFLIBTOOL}}

if [ ! -x "$AGENT" -o ! -x "$CF_PROMISES" -o ! -x "$CF_SERVERD" -o ! -x "$CF_KEY" -o ! -x "$RPMVERCMP" ]
then
    echo "ERROR can't find cf-agent or other binary. Are you sure you're running this from OBJDIR or OBJDIR/tests/acceptance?  Check '$AGENT', '$CF_PROMISES', '$CF_SERVERD', '$CF_KEY' and '$RPMVERCMP'"
    exit 1
fi

if [ "$UNSAFE_TESTS" = "1" ]
then
    if [ "$GAINROOT" = "fakeroot" ]
    then
        echo "Unsafe tests do not play well together with fakeroot. Please use a different"
        echo "--gainroot (like \"sudo\"), or you will get incorrect results."
        exit 1
    fi

    # Make sure test dir is accessible to everyone, because unsafe tests may
    # switch user during the test (users promises do this).
    DIR="$(cd "$(dirname "$0")"; pwd)"
    while [ "$DIR" != "/" -a "$DIR" != "" ]
    do
        $GAINROOT chmod go+rx "$DIR"
        DIR="$(dirname "$DIR")"
    done
fi

if [ $# -gt 0 ]
then
    # We run all specified tests according to the defaults (no unsafe ones).
    for test in "$@"
    do
        if ! expr "$test" : '[/.]' >/dev/null
        then
            test="./$test"
        fi

        if [ -f $test ]
        then
            ALL_TESTS="$ALL_TESTS${ALL_TESTS:+ }$test"
        elif [ -d $test ]
        then
            ALL_TESTS="$ALL_TESTS${ALL_TESTS:+ }$(find "$test" -name workdir -prune -o -name '*.cf' -print | sort)"
        else
            echo "Unable to open test file/directory: $test"
        fi
    done
else
    ALL_TESTS="$ALL_TESTS${ALL_TESTS:+ }$(find . -name workdir -prune -o -name '*.cf' -print | sort)"
fi

for addtest in $ALL_TESTS
do
    if echo "$addtest" | fgrep "/timed/" > /dev/null
    then
        if [ "$TIMED_TESTS" = 1 ]
        then
            eval TESTS_TIMED_$TEST_TIMED_INDEX="$addtest"
            eval TESTS_TIMEOUT_$TEST_TIMED_INDEX=0
            eval TESTS_PASSES_$TEST_TIMED_INDEX=0
            TEST_TIMED_INDEX=$(($TEST_TIMED_INDEX+1))
        fi
    else
        eval TESTS_$TEST_INDEX="$addtest"
        TEST_INDEX=$(($TEST_INDEX+1))
    fi

done

TESTS_NORMAL_COUNT=$TEST_INDEX
TESTS_TIMED_COUNT=$TEST_TIMED_INDEX
TESTS_COUNT=$(($TESTS_NORMAL_COUNT + $TESTS_TIMED_COUNT))
TESTS_TIMED_REMAINING=$TEST_TIMED_INDEX

#
# fd 7 is a /dev/null for quiet execution and stdout for default one
#
if [ -z "$QUIET" ]
then
    exec 7>&1
else
    exec 7>/dev/null
fi

print_header() {
    ( echo ======================================================================
        echo Testsuite started at $(date "+%Y-%m-%d %T")
        echo ----------------------------------------------------------------------
        echo Total tests: $TESTS_COUNT
        echo
        for feature in CRASHING_TESTS NETWORK_TESTS STAGING_TESTS UNSAFE_TESTS LIBXML2_TESTS
        do
            if eval "[ \${$feature} -ne 0 ]"
            then
                echo $feature: enabled
            else
                echo $feature: disabled
            fi
        done
        echo
    ) | tee "$LOG" | tee "$SUMMARY" >&7
}

print_footer() {
    # Recalculate results since sub invocations may not have been recorded.
    PASSED_TESTS=`egrep '^Pass ' $SUMMARY | wc -l | tr -d ' '`
    FAILED_TESTS=`egrep '^(FAIL|XFAIL) ' $SUMMARY | wc -l | tr -d ' '`
    SUPPRESSED_FAILURES=`egrep '^XFAIL ' $SUMMARY | wc -l | tr -d ' '`
    SOFT_FAILURES=`egrep '^SFAIL ' $SUMMARY | wc -l | tr -d ' '`
    SKIPPED_TESTS=`egrep '^Skip ' $SUMMARY | wc -l | tr -d ' '`

    ( echo
        echo ======================================================================
        echo "Testsuite finished at $(date  "+%F %T") ($(($END_TIME - $START_TIME)) seconds)"
    ) | tee -a "$LOG" | tee -a "$SUMMARY" >&7

    ( echo
        echo   "Passed tests:  $PASSED_TESTS"
        printf "Failed tests:  $FAILED_TESTS"
        if [ $SUPPRESSED_FAILURES -gt 0 ]
        then
            echo " ($SUPPRESSED_FAILURES are known and suppressed)"
        else
            echo
        fi
        echo   "Skipped tests: $SKIPPED_TESTS"
        echo   "Soft failures: $SOFT_FAILURES"
        echo   "Total tests:   $TESTS_COUNT"
    ) | tee -a "$LOG" | tee -a "$SUMMARY"

    if [ -n "$PRINTLOG" ]
    then
        cat "$LOG"
    fi
}

finish_xml() {
    mv "$XML" xml.tmp
    (
        cat <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="$(pwd)"
           timestamp="$(date "+%F %T")"
           hostname="localhost"
           tests="$TESTS_COUNT"
           failures="$FAILED_TESTS"
           skipped="$SKIPPED_TESTS"
           time="$(($END_TIME - $START_TIME)) seconds">
EOF
    cat xml.tmp
    cat <<EOF
</testsuite>
EOF
    ) > "$XML"
    rm -f xml.tmp
}

collect_results()
{
    if [ -n "$STAY_IN_WORKDIR" ]
    then
        return 0;
    fi

    WORKDIR="$(workdir "$1")"
    for file in "$LOG" "$SUMMARY" "$XML"
    do
        if [ -e "$WORKDIR/$file" ]
        then
            cat "$WORKDIR/$file" >> "$file"
            if [ "$NO_CLEAN" = "0" ]
            then
                rm -f "$WORKDIR/$file"
            fi
        fi
    done
    rmdir "$WORKDIR" >/dev/null 2>&1 || true
}

check_and_run_timed_tests() {
    TEST_TIMED_INDEX=0
    time=$(unix_seconds)
    # Run timed tests if any deadlines have expired.
    while [ $TEST_TIMED_INDEX -lt $TESTS_TIMED_COUNT ]
    do
        eval test=\$TESTS_TIMED_$TEST_TIMED_INDEX
        eval timeout=\$TESTS_TIMEOUT_$TEST_TIMED_INDEX
        if [ -n "$timeout" ] && [ "$time" -ge "$timeout" ]
        then
            eval TESTS_PASSES_$TEST_TIMED_INDEX="\$((\$TESTS_PASSES_$TEST_TIMED_INDEX+1))"
            eval pass=\$TESTS_PASSES_$TEST_TIMED_INDEX
            runtest "$AGENT" "$test" "$pass" "TESTS_TIMEOUT_$TEST_TIMED_INDEX"
            collect_results "$test"
            eval timeout=\$TESTS_TIMEOUT_$TEST_TIMED_INDEX
            if [ -z "$timeout" ]
            then
                TESTS_TIMED_REMAINING=$(($TESTS_TIMED_REMAINING - 1))
            fi
        fi
        TEST_TIMED_INDEX=$(($TEST_TIMED_INDEX+1))
    done
}

run_all_tests() {
    TEST_INDEX=0
    while [ $TEST_INDEX -lt $TESTS_NORMAL_COUNT -o $TESTS_TIMED_REMAINING -gt 0 ]
    do
        check_and_run_timed_tests

        # Run normal test.
        if [ $TEST_INDEX -lt $TESTS_NORMAL_COUNT ]
        then
            eval test=\$TESTS_$TEST_INDEX
            runtest "$AGENT" "$test"
            collect_results "$test"
            TEST_INDEX=$(($TEST_INDEX+1))
        elif [ $TESTS_TIMED_REMAINING -gt 0 ]
        then
            sleep 1
        fi
    done
}

run_all_tests_using_make() {
    MAKEFILE=Makefile.testall

    TEST_BLOCKS=1
    LAST_WAS_SERIAL=0

    for curr_test in $ALL_TESTS
    do
        # Keep serial tests after preceding tests and before following tests.
        case "$curr_test" in
            */serial_*|*_serial_*|*_serial.*|*/serial/*|*/unsafe/*)
                if [ $LAST_WAS_SERIAL = 0 ]
                then
                    TEST_BLOCKS=$(($TEST_BLOCKS + 1))
                    LAST_WAS_SERIAL=1
                fi
                ;;
            *)
                if [ $LAST_WAS_SERIAL = 1 ]
                then
                    TEST_BLOCKS=$(($TEST_BLOCKS + 1))
                    LAST_WAS_SERIAL=0
                fi
                ;;
        esac
        case "$curr_test" in
            */serial_*|*_serial_*|*_serial.*|*/serial/*|*/unsafe/*|*/timed/*)
                eval MAKE_RECIPE_LIST$TEST_BLOCKS='$MAKE_RECIPE_LIST'$TEST_BLOCKS'${MAKE_RECIPE_LIST'$TEST_BLOCKS':+ }$curr_test'
                ;;
            *)
                # Separate make target list (notice the "_rule"), because we do not want the target to be a
                # file name, since make will skip it if it exists (which of course it does).
                eval MAKE_TARGET_LIST$TEST_BLOCKS='$MAKE_TARGET_LIST'$TEST_BLOCKS'${MAKE_TARGET_LIST'$TEST_BLOCKS':+ }${curr_test}_rule'
                ;;
        esac
    done

    # Redirect to makefile all at once.
    (
        printf "tests:\n\n"
        printf "parallel_block0:\n\n"
        printf "serial_block0:\n\n"
        printf "tests: serial_block0 parallel_block0\n\n"

        i=1
        while [ $i -le $TEST_BLOCKS ]
        do
            if eval test -n '"$MAKE_RECIPE_LIST'$i'"'
            then
                eval printf '"serial_block$i:\n\t@-$0 --stay-in-workdir -j1 $MAKE_RECIPE_LIST'$i'\n\n"'
            fi
            printf "serial_block$i: serial_block$(($i-1)) parallel_block$(($i-1))\n\n"
            eval printf '"parallel_block$i: $MAKE_TARGET_LIST'$i'\n\n"'
            if eval test -n '"$MAKE_TARGET_LIST'$i'"'
            then
                eval printf '"$MAKE_TARGET_LIST'$i': serial_block$(($i-1)) parallel_block$(($i-1))\n\n"'
            fi
            printf "tests: serial_block$i parallel_block$i\n\n"

            i=$(($i + 1))
        done

        printf "%%_rule: %%\n\t@-$0 --stay-in-workdir -j1 \$<\n\n"
    ) > $MAKEFILE

    ${MAKE:-make} -k -f $MAKEFILE

    for curr_test in $ALL_TESTS
    do
        collect_results "$curr_test"
    done
}

#
# Now run the tests
#

# isEnabled? USE_INOTIFYWATCH
if [ -n "$USE_INOTIFYWATCH" ]
then
    if which inotifywatch 2>&1 > /dev/null
    then
        mkdir -p "$BASE_WORKDIR" 2> /dev/null
        inotifywatch ${INOTIFYWATCH_OPTS} $BASE_WORKDIR < /dev/null 2>&1 > ${INOTIFYWATCH_LOG} &
        INOTIFYWATCH_PID=$!
    else
        echo "info: inotifywatch not detected."
        exit 0
    fi
fi

# isEnabled? USE_INOTIFYWAIT
if [ -n "$USE_INOTIFYWAIT" ]
then
    if which inotifywait 2>&1 > /dev/null
    then
        mkdir -p "$BASE_WORKDIR" 2> /dev/null
        inotifywait ${INOTIFYWAIT_OPTS} $BASE_WORKDIR < /dev/null 2>&1 > ${INOTIFYWAIT_LOG} &
        INOTIFYWAIT_PID=$!
    else
        echo "info: inotifywait not detected."
        exit 0
    fi
fi

trap_handler() {
    [ -n "$INOTIFYWATCH_PID" ] && kill -9 ${INOTIFYWATCH_PID} 2>&1 > /dev/null
    [ -n "$INOTIFYWAIT_PID"  ] && kill -9 ${INOTIFYWAIT_PID}  2>&1 > /dev/null
    exit 0
}
if [ -n "$USE_INOTIFYWATCH" -o -n "$USE_INOTIFYWAIT"  ]
then
    trap trap_handler EXIT INT TERM
fi

START_TIME=$(unix_seconds)
if [ -z "$STAY_IN_WORKDIR" ]
then
    # This is top level invocation.
    print_header
    rm -f "$XML"
fi
if [ -z "$PARALLEL" -o "$TESTS_COUNT" -eq 1 ]
then
    run_all_tests
else
    run_all_tests_using_make
fi
END_TIME=$(unix_seconds)
if [ -z "$STAY_IN_WORKDIR" ]
then
    # This is top level invocation.
    print_footer
    finish_xml
fi

if [ $(($PASSED_TESTS+$FAILED_TESTS+$SOFT_FAILURES+$SKIPPED_TESTS)) -ne $TESTS_COUNT ]
then
    echo "WARNING: Number of test results does not match number of tests!"
    echo "Did something go wrong during execution?"
    exit 2
fi
if [ "$FAILED_TESTS" -gt "$SUPPRESSED_FAILURES" ]
then
    exit 1
else
    exit 0
fi

