#!/bin/bash

# Simple test wrapper around radclient to allow automated UATs
#
# Author Arran Cudbard-Bell <a.cudbardb@freeradius.org>
# Copyright 2014-2015 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
# Copyright 2015 The FreeRADIUS Project

# A POSIX variable
OPTIND=1         # Reset in case getopts has been used previously in the shell.

# Environmental variables
: ${TESTDIR=$(dirname $0)"/tests"}
: ${RADCLIENT='radclient'}
: ${FILTER_SUFFIX='_expected'}
# What we send back in response to a CoA request
: ${COA_REPLY_SUFFIX='_coa_reply'}
# What we expect from a CoA request
: ${COA_FILTER_SUFFIX='_coa_expected'}
PATH="$(dirname $0)/bin:${PATH}"

# Initialize our own variables
verbose=0
cluster=
role=
type=
parallel=40
retries=3
timeout=2
target='127.0.0.1'
secret='testing123'

# Some very basic logging functions
function ERROR
{
    echo "$@" 1>&2;
}

function INFO
{
    echo "$@"
}

function DEBUG
{
    if [ $verbose -gt 0 ]; then
        echo "$@"
    fi
}

function show_help
{
    echo $(basename $0)" [options] [-- <test_glob0> <test_glob1> <test_globN>]"
    echo "  -h                        Display this help message."
    echo "  -H <host>[:port]          Send test packets to specified host and port (defaults to 127.0.0.1)"
    echo "  -v                        Verbose mode."
    echo "  -p <number>               Run tests in parallel (defaults to 20)."
    echo "  -s <secret>               Shared secret."
    if [ ! -z "$role_types" ]; then
        echo "  -c <cluster>              Specify cluster type one of ($cluster_types)."
        echo "  -r <type>                 Specify server role one of ($role_types)."
        echo
        echo "Note: Test path globs are relative to ${TESTDIR}/<cluster>/<type>/"
    fi

    echo
    echo "For role based test file layout create test files under ${TESTDIR}/<cluster>/<type>"
    echo "Where <cluster> and <type> are substrings found in the FQDN of <host>."
    echo "For simplified test layout create test files under ${TESTDIR}"
    echo
    echo "The directory containing the tests should contains pairs of request files and filter files."
    echo "The request file name must contain 'test<num><num><num>."
    echo "The filter name must match the test name but with a '${FILTER_SUFFIX}' suffix."
    echo "For example:"
    echo "  ${TESTDIR}/test000_my_first_test"
    echo "  ${TESTDIR}/test000_my_first_test${FILTER_SUFFIX}"
    echo
    echo "It is also possible to specify a CoA reply and filter file."
    echo "The CoA-Request must be sent to port 3799 on the machine raduat is executing on."
    echo "The contents of the CoA reply file will be sent back to the RADIUS server."
    echo "An optional CoA filter file can be specified to check the contents of the CoA request."
    echo
    echo "The CoA reply file name must match the test name but with a '${COA_REPLY_SUFFIX}' suffix."
    echo "The CoA filter file name must match the test name but with a '${COA_FILTER_SUFFIX}' suffix."
    echo "For example:"
    echo "  ${TESTDIR}/test000_my_first_test${COA_REPLY_SUFFIX}"
    echo "  ${TESTDIR}/test000_my_first_test${COA_FILTER_SUFFIX}"
    echo
    echo "The following types of tests will be executed serially:"
    echo "  - Tests without an expected response"
    echo "  - Tests with CoA replies"
    echo "  - Tests with CoA filters"
    echo "  - Tests with \"# serial\" in the first line of the test file"
    echo
    echo "The directory containing the tests may have multiple subdirectories to group the tests."
}

RADCLIENT=$(which "$RADCLIENT")
if [ ! -x "$RADCLIENT" ]; then
    ERROR "Can't find radclient binary, modify your \$PATH or set RADCLIENT"
    exit 64
fi

if [ ! -d "$TESTDIR" ]; then
    ERROR "Test dir $TESTDIR does not exist, create it or specify it with TESTDIR=<dir>"
    show_help
    exit 64
fi

# Definitions (build these dynamically by looking at the files under tests)
cluster_dirs=$(find "$TESTDIR/" -mindepth 1 -maxdepth 1 -type d)
cluster_types=$(echo $cluster_dirs | sed 's/\s/ /g')

role_types=
for i in $cluster_dirs; do
    for j in $(find "$TESTDIR/$(basename $i)/" -mindepth 1 -maxdepth 1 -type d); do
        role=$(basename "$j")
        if [ "$role_types" == '' ]; then
            role_types="$role"
        else
            role_types+="\n$role"
        fi
    done
done

if [ -z "$role_types" ]; then
    DEBUG "Using simple test file layout"
else
    DEBUG "Using role based test file layout"
    role_types=$(echo -e "$role_types" | sort | uniq)   # Remove duplicates
    role_types=$(echo $role_types | sed 's/\s/ /g')     # Change \n back to spaces
fi

while getopts "h?H:vc:r:s:p:" opt; do
    case "$opt" in
    h|\?)
        show_help
        exit 0
        ;;

    v)
        verbose=1
        ;;

    c)
        found=0
        for i in $cluster_types; do
            if [ "$i" == "$OPTARG" ]; then
                found=1
            fi
        done
        if [ $found -ne 1 ]; then
            ERROR "'$OPTARG' is not a valid cluster type"
            show_help
            exit 64
        fi
        cluster="$OPTARG"
        ;;

    r)
        found=0
        for i in $role_types; do
            if [ "$i" == "$OPTARG" ]; then
                found=1
            fi
        done
        if [ $found -ne 1 ]; then
            ERROR "'$OPTARG' is not a valid role type"
            show_help
            exit 64
        fi
        role="$OPTARG"
        ;;

    s)
        secret="$OPTARG"
        ;;

    p)
        if ! echo "$OPTARG" | grep -E '^[0-9]+$' > /dev/null; then
            ERROR "Non integer argument '$OPTARG' specified for -p"
            show_help
            exit 64
        fi
        parallel=$OPTARG
        ;;

    H)
        target="$OPTARG"
        ;;

    esac
done

shift $((OPTIND-1))

[ "$1" = "--" ] && shift
test_files=$@

#
#  Match keywords from the hostname to clusters or roles
#
if [ ! -z "$role_types" ]; then
    this_host=$(hostname -f)
    for tok in $(echo "$this_host" | sed 's/\./ /g'); do
        for key in ${cluster_types}; do
            if echo "$tok" | grep "$key" > /dev/null && [ "$cluster" = '' ]; then cluster="$key"; fi
        done
        for key in ${role_types}; do
            if echo "$tok" | grep "$key" > /dev/null && [ "$role" = '' ]; then role="$key"; fi
        done
    done

    if [ "$cluster" == '' ]; then
        ERROR "Couldn't determine the cluster $this_host belongs to";
        show_help
        exit 64;
    fi

    if [ "$role" == '' ]; then
        ERROR "Couldn't determine the role $this_host performs";
        show_help
        exit 64;
    fi

    test_path="${TESTDIR}/${cluster}/${role}"
#
#  Otherwise just use the tests in the test dir
#
else
    test_path="${TESTDIR}"
fi

if [ "$test_files" != '' ]; then
    tmp=
    for glob in $test_files; do
        # Filter out response files (makes wildcards easier), and expand the globs
        for file in $(find "${test_path}" -depth -path "*${glob}" \
            -and -not -path "*${FILTER_SUFFIX}" \
            -and -not -path "*${COA_REPLY_SUFFIX}" \
            -and -not -path "*${COA_FILTER_SUFFIX}" \
            -and '(' -type f -or -type l ')'); do
            tmp+="${file} "
        done
    done
    test_files="${tmp}"
else
    # Lexicographical, depth-first
    test_files=$(find "$test_path" -depth -path '*test[0-9][0-9][0-9]*' \
                -and -not -path "*${FILTER_SUFFIX}" \
                -and -not -path "*${COA_REPLY_SUFFIX}" \
                -and -not -path "*${COA_FILTER_SUFFIX}" \
                -and '(' -type f -or -type l ')')
    if [ "$test_files" == '' ]; then
        ERROR "No test files found in $test_path"
        exit 64;
    fi
    INFO "Executing"$(echo "$test_files" | wc -l)" test(s) from ${test_path}"
fi

#
#  Check if we got any test files
#
if [ "$test_files" == '' ]; then
    ERROR "No test files to process"
    exit 1
fi

#
#  Output which files were going to be using for testing
#
if [ $verbose -eq 0 ]; then
    INFO "Executing specified tests"
    INFO "Use -v to see full list"
else
    INFO "Executing specified tests:"
    for i in $test_files; do
        DEBUG "$i"
    done
fi

#
#  Figure out which tests we can munge into a single file which we can
#  use to parallelise testing
#
base=$(basename $0)
packets=$(mktemp -t "${base}XXX") || exit 1
filters=$(mktemp -t "${base}XXX") || exit 1

args=
file_args=
serial_file_args=
for i in $test_files; do
    if [ ! -f "$i" -a ! -L "$i" ]; then
        INFO "Skipping $i: not file"
        continue
    fi

    if [ ! -r "$i" ]; then
        INFO "Skipping $i: not readable (check permissions)"
        continue
    fi

    expected="${i}${FILTER_SUFFIX}"
    # We need pairs of requests and responses in the bulk format
    if [ ! -f "$expected" -a ! -L "$expected" ]; then
        DEBUG "$i cannot be parallelised: Can't find 'expected' file"
        file_args+=" -f \"$i\""
        continue
    fi

    # We mostly do this to let radclient produce an error
    if [ ! -r "$expected" ]; then
        INFO "$i cannot be parallelised: 'expected' file not readable"
        file_args+=" -f \"${i}:${expected}\""
        continue
    fi

    # If there's a coa_reply file, then we need to serialise the test
    # because there's no bulk test format that includes coa_reply
    coa_reply="${i}${COA_REPLY_SUFFIX}"
    if [ -f "$coa_reply" -o -L "$coa_reply" ]; then
        DEBUG "$i cannot be parallelised: Found CoA Reply"

        # We also have a filter
        coa_filter="${i}${COA_FILTER_SUFFIX}"
        if [ -f "$coa_filter" -o -L "$coa_filter" ]; then
            serial_file_args+=" -f \"${i}:${expected}:${coa_reply}:${coa_filter}\""
        else
            serial_file_args+=" -f \"${i}:${expected}:${coa_reply}\""
        fi
        continue
    fi

    # If the test file is marked as serial only, then we need to serialise the test
    if head -n 1 "$i" | grep -i -E '^#\s*serial' > /dev/null; then
        DEBUG "$i marked as serial only"
        serial_file_args+=" -f \"${i}:${expected}\""
        continue
    fi

    # Else add it to the master test file
    printf '%s\n' "$(cat "$i")" >> "$packets"

    # Add the name of the file so it appears in radclient debug output
    # and can later be specified with -v -- <test> to drill down.
    echo "Radclient-Test-Name := \""$(echo "$i" | sed -e "s@${test_path}/\?@@")"\"" >> "$packets"
    echo >> "$packets"
    printf '%s\n' "$(cat "${i}_expected")" >> "$filters"
    echo >> "$filters"
done

if [ `cat "$packets" | wc -l` -gt 0 ]; then
    file_args+=" -f \"${packets}:${filters}\""
fi

if [ ! -z "$file_args" ]; then
	args="$file_args"

	if [ $verbose -ne 0 ]; then
    		args+=" -x"
	fi

	args+=" -s"
	args+=" -t \"$timeout\""
	args+=" -r \"$retries\""
	args+=" -p \"$parallel\""
	args+=" \"$target\""
	args+=" auto"
	args+=" \"$secret\""

	DEBUG "Executing: $RADCLIENT $args"
	eval $RADCLIENT $args; ret=$?
	INFO "(Parallelised tests)"
	INFO ""

	rm -f "$packets" 2>&1 > /dev/null
	rm -f "$filters" 2>&1 > /dev/null

	if [ $ret -ne 0 ]; then
    		ERROR "One or more tests failed (radclient exited with $ret)"
    		exit $ret
	fi
fi

if [ ! -z "$serial_file_args" ]; then
	args="$serial_file_args"

	if [ $verbose -ne 0 ]; then
    		args+=" -x"
	fi

	args+=" -s"
	args+=" -t \"$timeout\""
	args+=" -r \"$retries\""
	args+=" -p 1"
	args+=" \"$target\""
	args+=" auto"
	args+=" \"$secret\""

	DEBUG "Executing: $RADCLIENT $args"
	eval $RADCLIENT $args; ret=$?
	INFO "(Serialised tests)"

	if [ $ret -ne 0 ]; then
    		ERROR "One or more tests failed (radclient exited with $ret)"
    		exit $ret
	fi
fi

exit 0
