#!/bin/sh

# Copyright (C) 2018 Eaton
#
# 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 2 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
#! \file    nut-driver-enumerator-test.sh
#  \author  Jim Klimov <EvgenyKlimov@eaton.com>
#  \brief   Self-test for nut-driver-enumerator.sh utility
#  \details Automated sanity test for nut-driver-enumerator.sh(.in)
#           using different shells (per $SHELL_PROGS) and CLI requests
#           for regression and compatibility tests as well as for TDD
#           fueled by pre-decided expected outcomes.

### Use a standard locale setup so sorting in expected results is not confused
LANG=C
LC_ALL=C
TZ=UTC
export LANG LC_ALL TZ

### Note: These are relative to where the selftest script lives,
### not the NUT top_srcdir etc. They can be exported by a Makefile.
[ -n "${BUILDDIR-}" ] || BUILDDIR="`dirname $0`"
[ -n "${SRCDIR-}" ] || SRCDIR="`dirname $0`"
[ -n "${SHELL_PROGS-}" ] || SHELL_PROGS="/bin/sh"
case "${DEBUG-}" in
    [Yy]|[Yy][Ee][Ss]) DEBUG=yes ;;
    [Tt][Rr][Aa][Cc][Ee]) DEBUG=trace ;;
    *) DEBUG="" ;;
esac

SYSTEMD_CONFPATH="${BUILDDIR}/selftest-rw/systemd-units"
export SYSTEMD_CONFPATH

NUT_CONFPATH="${BUILDDIR}/selftest-rw/nut"
export NUT_CONFPATH

[ -n "${UPSCONF-}" ] || UPSCONF="${SRCDIR}/nut-driver-enumerator-test--ups.conf"
[ ! -s "${UPSCONF-}" ] && echo "FATAL : testing ups.conf not found as '$UPSCONF'" >&2 && exit 1
export UPSCONF

if [ ! -n "${NDE-}" ] ; then
    for NDE in \
        "${BUILDDIR}/../scripts/upsdrvsvcctl/nut-driver-enumerator.sh" \
        "${SRCDIR}/../scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in" \
    ; do [ -s "$NDE" ] && break ; done
fi
[ ! -s "${NDE-}" ] && echo "FATAL : testing nut-driver-enumerator.sh implementation not found as '$NDE'" >&2 && exit 1

# TODO : Add tests that generate configuration files for units
#mkdir -p "${NUT_CONFPATH}" "${SYSTEMD_CONFPATH}" || exit

FAIL_COUNT=0
GOOD_COUNT=0
callNDE() {
    case "$DEBUG" in
        yes)   time $USE_SHELL $NDE "$@" ;;
        trace) time $USE_SHELL -x $NDE "$@" ;;
        *)     $USE_SHELL $NDE "$@" 2>/dev/null ;;
    esac
}

run_testcase() {
    # First 3 args are required as defined below; the rest are
    # CLI arg(s) to nut-driver-enumerator.sh
    CASE_DESCR="$1"
    EXPECT_CODE="$2"
    EXPECT_TEXT="$3"
    shift 3

    printf "Testing : SHELL='%s'\tCASE='%s'\t" "$USE_SHELL" "$CASE_DESCR"
    OUT="`callNDE "$@"`" ; RESCODE=$?
    printf "Got : RESCODE='%s'\t" "$RESCODE"

    RES=0
    if [ "$RESCODE" = "$EXPECT_CODE" ]; then
        printf "STATUS_CODE='MATCHED'\t"
        GOOD_COUNT="`expr $GOOD_COUNT + 1`"
    else
        printf "STATUS_CODE='MISMATCH' expect_code=%s received_code=%s\t" "$EXPECT_CODE" "$RESCODE" >&2
        FAIL_COUNT="`expr $FAIL_COUNT + 1`"
        RES="`expr $RES + 1`"
    fi

    if [ "$OUT" = "$EXPECT_TEXT" ]; then
        printf "STATUS_TEXT='MATCHED'\n"
        GOOD_COUNT="`expr $GOOD_COUNT + 1`"
    else
        printf "STATUS_TEXT='MISMATCH'\n"
        printf '\t--- expected ---\n%s\n\t--- received ---\n%s\n\t--- MISMATCH ABOVE\n\n' "$EXPECT_TEXT" "$OUT" >&2
        # Give a nice output to help track the problem:
        ( rm -f "/tmp/.nde.text.expected.$$" "/tmp/.nde.text.actual.$$" \
            && echo "$EXPECT_TEXT" > "/tmp/.nde.text.expected.$$" \
            && echo "$OUT" > "/tmp/.nde.text.actual.$$" \
            && diff -u "/tmp/.nde.text.expected.$$" "/tmp/.nde.text.actual.$$" ) 2>/dev/null || true
        rm -f "/tmp/.nde.text.expected.$$" "/tmp/.nde.text.actual.$$"
        FAIL_COUNT="`expr $FAIL_COUNT + 1`"
        RES="`expr $RES + 2`"
    fi
    if [ "$RES" != 0 ] || [ -n "$DEBUG" ] ; then echo "" ; fi
    return $RES
}

##################################################################
# Note: expectations in test cases below are tightly connected   #
# to both the current code in the script and content of the test #
# configuration file.                                            #
##################################################################

testcase_bogus_args() {
    run_testcase "Reject unknown args" 1 "" \
        --some-bogus-arg
}

testcase_list_all_devices() {
    # We expect a list of unbracketed names from the device sections
    # Note: unlike other outputs, this list is alphabetically sorted
    run_testcase "List all device names from sections" 0 \
"dummy-proxy
dummy-proxy-localhost
dummy1
epdu-2
epdu-2-snmp
qx-serial
qx-usb1
qx-usb2
sectionWithComment
sectionWithCommentWhitespace
serial.4
usb_3
valueHasEquals
valueHasHashtag
valueHasQuotedHashtag" \
        --list-devices
}

testcase_show_all_configs() {
    # We expect whitespace trimmed, comment-only lines removed
    run_testcase "Show all configs" 0 \
'maxstartdelay=180
globalflag
[dummy1]
driver=dummy-ups
port=file1.dev
desc="This is ups-1"
[epdu-2]
driver=netxml-ups
port=http://172.16.1.2
synchronous=yes
[epdu-2-snmp]
driver=snmp-ups
port=172.16.1.2
synchronous=no
[usb_3]
driver=usbhid-ups
port=auto
[serial.4]
driver=serial-ups
driverflag
port=/dev/ttyS1 # some path
[dummy-proxy]
driver="dummy-ups  "
port=remoteUPS@RemoteHost.local
[dummy-proxy-localhost]
driver='"'dummy-ups  '"'
port=localUPS@127.0.0.1
[valueHasEquals]
driver=dummy=ups
port=file1.dev # key = val, right?
[valueHasHashtag]
driver=dummy-ups
port=file#1.dev
[valueHasQuotedHashtag]
driver=dummy-ups
port=file#1.dev
[qx-serial]
driver=nutdrv_qx
port=/dev/ttyb
[qx-usb1]
driver=nutdrv_qx
port=auto
[qx-usb2]
driver=nutdrv_qx
port=/dev/usb/8
[sectionWithComment]
driver=nutdrv_qx#comment
port=/dev/usb/8
desc="value with [brackets]"
[brackets with spaces are not sections] # but rather an invalid mess as binary parser may think
[sectionWithCommentWhitespace]
driver=nutdrv_qx	# comment
port=/dev/usb/8 #	comment
commentedDriverFlag # This flag gotta mean something' \
        --show-all-configs
}

testcase_upslist_debug() {
    # We expect a list of names, ports and decided MEDIA type (for dependencies)
    run_testcase "List decided MEDIA and config checksums for all devices" 0 \
"INST: 68b329da9893e34099c7d8ad5cb9c940~[]: DRV='' PORT='' MEDIA='' SECTIONMD5='9a1f372a850f1ee3ab1fc08b185783e0'
INST: 010cf0aed6dd49865bb49b70267946f5~[dummy-proxy]: DRV='dummy-ups  ' PORT='remoteUPS@RemoteHost.local' MEDIA='network' SECTIONMD5='aff543fc07d7fbf83e81001b181c8b97'
INST: 1ea79c6eea3681ba73cc695f3253e605~[dummy-proxy-localhost]: DRV='dummy-ups  ' PORT='localUPS@127.0.0.1' MEDIA='network-localhost' SECTIONMD5='73e6b7e3e3b73558dc15253d8cca51b2'
INST: 76b645e28b0b53122b4428f4ab9eb4b9~[dummy1]: DRV='dummy-ups' PORT='file1.dev' MEDIA='' SECTIONMD5='9e0a326b67e00d455494f8b4258a01f1'
INST: a293d65e62e89d6cc3ac6cb88bc312b8~[epdu-2]: DRV='netxml-ups' PORT='http://172.16.1.2' MEDIA='network' SECTIONMD5='0d9a0147dcf87c7c720e341170f69ed4'
INST: 9a5561464ff8c78dd7cb544740ce2adc~[epdu-2-snmp]: DRV='snmp-ups' PORT='172.16.1.2' MEDIA='network' SECTIONMD5='2631b6c21140cea0dd30bb88b942ce3f'
INST: 16adbdafb22d9fdff1d09038520eb32e~[qx-serial]: DRV='nutdrv_qx' PORT='/dev/ttyb' MEDIA='serial' SECTIONMD5='e3e6e586fbe5b3c0a89432f4b993f4ad'
INST: a21bd2b786228b9619f6adba6db8fa83~[qx-usb1]: DRV='nutdrv_qx' PORT='auto' MEDIA='usb' SECTIONMD5='a6139c5da35bef89dc5b96e2296f5369'
INST: 0066605e07c66043a17eccecbeea1ac5~[qx-usb2]: DRV='nutdrv_qx' PORT='/dev/usb/8' MEDIA='usb' SECTIONMD5='5722dd9c21d07a1f5bcb516dbc458deb'
INST: 1280a731e03116f77290e51dd2a2f37e~[sectionWithComment]: DRV='nutdrv_qx#comment' PORT='/dev/usb/8' MEDIA='' SECTIONMD5='be30e15e17d0579c85eecaf176b4a064'
INST: 770abd5659061a29ed3ae4f7c0b00915~[sectionWithCommentWhitespace]: DRV='nutdrv_qx	# comment' PORT='/dev/usb/8 #	comment' MEDIA='' SECTIONMD5='c757822a331521cdc97310d0241eba28'
INST: efdb1b4698215fdca36b9bc06d24661d~[serial.4]: DRV='serial-ups' PORT='/dev/ttyS1 # some path' MEDIA='' SECTIONMD5='9c485f733aa6d6c85c1724f162929443'
INST: f4a1c33db201c2ca897a3337993c10fc~[usb_3]: DRV='usbhid-ups' PORT='auto' MEDIA='usb' SECTIONMD5='1f6a24becde9bd31c9852610658ef84a'
INST: 8e5686f92a5ba11901996c813e7bb23d~[valueHasEquals]: DRV='dummy=ups' PORT='file1.dev # key = val, right?' MEDIA='' SECTIONMD5='2f04d65da53e3b13771bb65422f0f4c0'
INST: 99da99b1e301e84f34f349443aac545b~[valueHasHashtag]: DRV='dummy-ups' PORT='file#1.dev' MEDIA='' SECTIONMD5='6029bda216de0cf1e81bd55ebd4a0fff'
INST: d50c3281f9b68a94bf9df72a115fbb5c~[valueHasQuotedHashtag]: DRV='dummy-ups' PORT='file#1.dev' MEDIA='' SECTIONMD5='af59c3c0caaa68dcd796d7145ae403ee'" \
        upslist_debug

    # FIXME : in [valueHasEquals] and [serial.4] the PORT value is quite bogus
    # with its embedded comments. Check vs. binary config parser, whether in
    # unquoted case only first token is the valid value, and how comments are
    # handled in general?
    # FIXME : in [valueHasHashtag] the line after "#" should likely be dropped
    # (check in binary config parser first) while in [valueHasQuotedHashtag]
    # it should stay.
}

testcase_getValue() {
    run_testcase "Query a configuration key (SDP)" 0 \
        "file1.dev" \
        --show-device-config-value dummy1 port

    run_testcase "Query a configuration key (other)" 0 \
        "yes" \
        --show-device-config-value epdu-2 synchronous

    run_testcase "Query a configuration key (originally quoted)" 0 \
        'This is ups-1' \
        --show-device-config-value dummy1 desc

    run_testcase "Query a configuration flag (driver)" 0 \
        "driverflag" \
        --show-config-value 'serial.4' driverflag

    run_testcase "Query a missing configuration flag (driver)" 1 \
        "" \
        --show-config-value 'valueHasQuotedHashtag' nosuchflag

    run_testcase "Query multiple configuration keys (originally quoted)" 0 \
        'This is ups-1
file1.dev' \
        --show-device-config-value dummy1 desc port

    run_testcase "Query multiple configuration keys with some missing (originally quoted)" 1 \
        'This is ups-1

file1.dev' \
        --show-device-config-value dummy1 desc unknownkey port
}

testcase_globalSection() {
    run_testcase "Display global config" 0 \
        "maxstartdelay=180
globalflag" \
        --show-config ''

    run_testcase "Query a configuration key (global)" 0 \
        "180" \
        --show-config-value '' maxstartdelay

    run_testcase "Query a configuration flag (global)" 0 \
        "globalflag" \
        --show-config-value '' globalflag

    run_testcase "Query a missing configuration flag (global)" 1 \
        "" \
        --show-config-value '' nosuchflag
}


# Combine the cases above into a stack
testsuite() {
    testcase_bogus_args
    testcase_list_all_devices
    testcase_show_all_configs
    testcase_getValue
    testcase_globalSection
    # This one can take a while, put it last
    testcase_upslist_debug
}

# If no args...
for USE_SHELL in $SHELL_PROGS ; do
    case "$USE_SHELL" in
        busybox|busybox_sh) USE_SHELL="busybox sh" ;;
    esac
    testsuite
done
# End of loop over shells

echo "Test suite for nut-driver-enumerator has completed with $FAIL_COUNT failed cases and $GOOD_COUNT good cases" >&2

[ "$FAIL_COUNT" = 0 ] || { echo "As a developer, you may want to export DEBUG=trace or export DEBUG=yes and re-run the test; also make sure you meant the nut-driver-enumerator.sh implementation as NDE='$NDE'" >&2 ; exit 1; }