#!/bin/sh

: ${TEST_BOARDS_AVAILABLE:="esp32-wroom-32 samr21-xpro"}

# temporarily disabling llvm builds until https://github.com/RIOT-OS/RIOT/pull/15595
# is in
#: ${TEST_BOARDS_LLVM_COMPILE:="iotlab-m3 native nrf52dk mulle nucleo-f401re samr21-xpro slstk3402a"}
: ${TEST_BOARDS_LLVM_COMPILE:=""}

: ${TEST_KCONFIG_samr21_xpro:="examples/hello-world tests/periph_*
tests/test_tools tests/congure_* tests/xtimer_* tests/ztimer_*
tests/driver_ad7746 tests/driver_adcxx1c tests/driver_ads101x tests/driver_adt101x
tests/driver_adt7310 tests/driver_adxl345 tests/driver_aip31068 tests/driver_apa102
tests/driver_apds99xx tests/driver_apds99xx_full tests/driver_at tests/driver_at24cxxx
tests/driver_at24mac tests/driver_at25xxx tests/driver_at30tse75x tests/driver_ata8520e
tests/driver_b* tests/driver_ccs811 tests/driver_ccs811_full tests/driver_dcf77
tests/driver_dfplayer tests/driver_dht tests/driver_ds18 tests/driver_ds75lx
tests/driver_ds1307 tests/driver_ds3231 tests/driver_ds3234 tests/driver_dsp0401
tests/driver_dynamixel tests/driver_edbg_eui tests/driver_f* tests/driver_g*
tests/driver_h* tests/driver_i* tests/driver_j* tests/driver_l*
tests/driver_mag3110 tests/driver_mhz19 tests/driver_mma7660
tests/driver_motor_driver tests/driver_mpl3115a2 tests/driver_mpu9x50
tests/driver_mq3 tests/driver_my9221 tests/driver_nvram_spi tests/mtd_flashpage
tests/mtd_mapper tests/driver_o* tests/driver_p* tests/driver_q*
tests/driver_r* tests/driver_s* tests/driver_t* tests/driver_u*
tests/driver_v*"}
: ${TEST_KCONFIG_native:="examples/hello-world
tests/cb_mux*
tests/congure_*
tests/driver_ws281x
tests/eepreg
tests/periph_*
tests/pkg_c25519
tests/pkg_cayenne-lpp
tests/pkg_cifra
tests/pkg_cn-cbor
tests/pkg_emlearn
tests/pkg_hacl
tests/pkg_heatshrink
tests/pkg_jsmn
tests/pkg_libb2
tests/pkg_libcose
tests/pkg_libfixmath
tests/pkg_libhydrogen
tests/pkg_lora-serialization
tests/pkg_micro-ecc
tests/pkg_minmea
tests/pkg_monocypher
tests/pkg_nanocbor
tests/pkg_nanopb
tests/pkg_qDSA
tests/pkg_qcbor
tests/pkg_relic
tests/pkg_tiny-asn1
tests/pkg_tinycbor
tests/pkg_tinycrypt
tests/pkg_tweetnacl
tests/pkg_umorse
tests/pkg_yxml
tests/posix_sleep
tests/prng_*
tests/shell
tests/struct_tm_utility
tests/sys_crypto
tests/test_tools
tests/xtimer_*
tests/ztimer_*
"}

: ${TEST_WITH_CONFIG_SUPPORTED:="examples/suit_update tests/driver_at86rf2xx_aes"}

export RIOT_CI_BUILD=1
export CC_NOCOLOR=1
export STATIC_TESTS=0
export CFLAGS_DBG=""
export DLCACHE_DIR=${DLCACHE_DIR:-~/.dlcache}
export ENABLE_TEST_CACHE=${ENABLE_TEST_CACHE:-1}

# This is a work around for a bug in CCACHE which interacts very badly with
# some features of RIOT and of murdock. The result is that ccache is
# ineffective (i.e. objects are never reused, resulting in extreme cache miss
# rate) and murdock becomes slow.
#
# - CCACHE thinks that -gz by itself enables debugging, which is not true.
#   see https://github.com/ccache/ccache/issues/464
#   - When debug info is included, CCACHE hashes the file paths, as these
#     influence the debug information (the name of compile units and/or their
#     "comp_dir" attribute)
# - Riot does not set -fdebug-prefix-map. This is not that easy as it may not
#   be supported in every toolchain (some are quite old).
# - Murdock builds PRs in different directories each time.
#
# It is only the combination of these three factors which causes this bug.
export OPTIONAL_CFLAGS_BLACKLIST="-gz"

NIGHTLY=${NIGHTLY:-0}
RUN_TESTS=${RUN_TESTS:-${NIGHTLY}}

DWQ_ENV="-E BOARDS -E APPS -E NIGHTLY -E RUN_TESTS -E ENABLE_TEST_CACHE
         -E TEST_HASH -E CI_PULL_LABELS"

get_kconfig_test_apps() {
    case "$1" in
        "samr21-xpro") echo "${TEST_KCONFIG_samr21_xpro}" ;;
    esac
    case "$1" in
        "native") echo "${TEST_KCONFIG_native}" ;;
    esac
}

check_label() {
    local label="${1}"
    [ -z "${CI_PULL_LABELS}" ] && return 1
    echo "${CI_PULL_LABELS}" | grep -q "${label}"
    return $?
}

[ "$RUN_TESTS" != "1" ] && {
    check_label "CI: run tests" && RUN_TESTS=1
}

[ "$ENABLE_TEST_CACHE" = "1" ] && {
    check_label "CI: disable test cache" && export ENABLE_TEST_CACHE=0
}

error() {
    echo "$@"
    exit 1
}

# true if "$2" starts with "$1", false otherwise
startswith() {
  case "${2}" in
    ${1}*) true ;;
    *) false ;;
  esac
}

# if MURDOCK_HOOK is set, this function will execute it and pass on all it's
# parameters. should the hook script exit with negative exit code, hook() makes
# this script exit with error, too.
# hook() will be called from different locations of this script.
# currently, the only caller is "run_test", which calls "hook run_test_pre".
# More hooks will be added as needed.
hook() {
    if [ -n "${MURDOCK_HOOK}" ]; then
        echo "- executing hook $1"
        "${MURDOCK_HOOK}" "$@" || {
            error "$0: hook \"${MURDOCK_HOOK} $@\" failed!"
        }
        echo "- hook $1 finished"
    fi
}

# true if word "$1" is in list of words "$2", false otherwise
# uses grep -w, thus only alphanum and "_" count as word bounderies
# (word "def" matches "abc-def")
is_in_list() {
    [ $# -ne 2 ] && return 1

    local needle="$1"
    local haystack="$2"

    echo "$haystack" | grep -q -w "$needle"
}

# grep that doesn't return error on empty input
_grep() {
    grep "$@"
    true
}

_greplist() {
    if [ $# -eq 0 ]; then
        echo cat
    else
        echo -n "_grep -E ($1"
        shift
        for i in $*; do
            echo -n "|$i"
        done
        echo ")"
    fi
}

# get list of all app directories
get_apps() {
    make -f makefiles/app_dirs.inc.mk info-applications \
        | $(_greplist $APPS) | sort
}

# take app dir as parameter, print all boards that are supported
# Only print for boards in $BOARDS.
get_supported_boards() {
    local appdir=$1
    local boards="$(make --no-print-directory -C$appdir info-boards-supported 2>/dev/null || echo broken)"

    if [ "$boards" = broken ]; then
        echo "makefile_broken"
        return
    fi

    for board in $boards
    do
        echo $board
    done | $(_greplist $BOARDS)
}

get_supported_toolchains() {
    local appdir=$1
    local board=$2
    local toolchains="gnu"

    if is_in_list "${board}" "${TEST_BOARDS_LLVM_COMPILE}"; then
        toolchains="$(make -s --no-print-directory -C${appdir} BOARD=${board} \
                      info-toolchains-supported 2> /dev/null | grep -o -e "llvm" -e "gnu")"
    fi
    echo "${toolchains}"
}

# given an app dir as parameter, print "$appdir $board:$toolchain" for each
# supported board and toolchain. Only print for boards in $BOARDS.
# if extra args are given, they will be prepended to each output line.
get_app_board_toolchain_pairs() {
    local appdir=$1
    local boards="$(get_supported_boards $appdir)"

    # collect extra arguments into prefix variable
    shift
    local prefix="$*"

    if [ "$boards" = makefile_broken ]; then
        echo "$appdir makefile_broken"
        return
    fi

    for board in ${boards}
    do
        for toolchain in $(get_supported_toolchains $appdir $board)
        do
            echo $prefix $appdir $board:$toolchain
        done
    done | $(_greplist $BOARDS)
}

# use dwqc to create full "appdir board toolchain" compile job list
get_compile_jobs() {
    check_label "CI: skip compile test" && return
    get_apps | \
        dwqc ${DWQ_ENV} -s \
        ${DWQ_JOBID:+--subjob} \
        "$0 get_app_board_toolchain_pairs \${1} $0 compile"
}

print_worker() {
    [ -n "$DWQ_WORKER" ] && \
        echo "-- running on worker ${DWQ_WORKER} thread ${DWQ_WORKER_THREAD}, build number $DWQ_WORKER_BUILDNUM."
}

test_hash_calc() {
    local bindir=$1

    # Why two times cut?
    # "test-input-hash.sha1" contains a list of lines containing
    # "<hash> <filename>" on each line.
    # We need to filter out the filename, as it contains the full path name,
    # which differs depending on the build machine.
    #
    # After piping through sha1sum, we get "<hash> -". " -" has to go so we save the
    # hassle of escaping the resulting hash.

    cat ${bindir}/test-input-hash.sha1 | cut -f1 -d' ' | sha1sum | cut -f1 -d' '
}

test_cache_get() {
    test "${ENABLE_TEST_CACHE}" = "1" || return 1
    test -n "$(redis-cli get $1)" > /dev/null
}

test_cache_put() {
    redis-cli set "$1" ok
}

# compile one app for one board with one toolchain. delete intermediates.
compile() {
    local appdir=$1
    local board=$(echo $2 | cut -f 1 -d':')
    local toolchain=$(echo $2 | cut -f 2 -d':')

    [ "$board" = "makefile_broken" ] && {
        echo "$0: There seems to be a problem in \"$appdir\" while getting supported boards!"
        echo "$0: testing \"make -C$appdir info-boards-supported\"..."
        make -C$appdir info-boards-supported && echo "$0: success. no idea what's wrong." || echo "$0: failed!"
        exit 1
    }

    # set build directory. CI ensures only one build at a time in $(pwd).
    export BINDIR="$(pwd)/build"
    export PKGDIRBASE="${BINDIR}/pkg"

    # Pre-build cleanup
    rm -rf ${BINDIR}

    print_worker

    # sanity checks
    [ $# -ne 2 ] && error "$0: compile: invalid parameters (expected \$appdir \$board:\$toolchain)"
    [ ! -d "$appdir" ] && error "$0: compile: error: application directory \"$appdir\" doesn't exist"

    # We compile a first time with Kconfig based dependency
    # resolution for regression purposes. $TEST_KCONFIG contains a
    # list of board-application tuples that are currently modeled to
    # run with Kconfig

    should_check_kconfig_hash=0

    for app in $(get_kconfig_test_apps "${board}")
    do
        if [ "${appdir}" = "${app}" ]; then
            should_check_kconfig_hash=1
            BOARD=${board} make -C${appdir} clean
            CCACHE_BASEDIR="$(pwd)" BOARD=${board} TOOLCHAIN=${toolchain} RIOT_CI_BUILD=1 TEST_KCONFIG=1 \
                            make -C${appdir} all test-input-hash -j${JOBS:-4}
            RES=$?
            if [ $RES -eq 0 ]; then
                kconfig_test_hash=$(test_hash_calc "${BINDIR}")
            else
                kconfig_test_hash=0
                echo "An error occurred while compiling using Kconfig";
            fi
            break
        fi
    done

    # compile without Kconfig
    CCACHE_BASEDIR="$(pwd)" BOARD=$board TOOLCHAIN=$toolchain RIOT_CI_BUILD=1 \
        make -C${appdir} clean all test-input-hash -j${JOBS:-4}
    RES=$?

    test_hash=$(test_hash_calc "$BINDIR")

    if [ ${should_check_kconfig_hash} != 0 ]; then
        if [ ${kconfig_test_hash} != ${test_hash} ]; then
            echo "Hashes of binaries with and without Kconfig mismatch for ${appdir}";
            echo "Please check that all used modules are modelled in Kconfig and enabled";
            RES=1
        fi
    fi

    # run tests
    if [ $RES -eq 0 ]; then
        if [ $RUN_TESTS -eq 1 -o "$board" = "native" ]; then
            if [ -f "${BINDIR}/.test" ]; then
                if [ "$board" = "native" ]; then
                    BOARD=$board make -C${appdir} test
                    RES=$?
                elif is_in_list "$board" "$TEST_BOARDS_AVAILABLE"; then
                    echo "-- test_hash=$test_hash"
                    if test_cache_get $test_hash; then
                        echo "-- skipping test due to positive cache hit"
                    else
                        BOARD=$board TOOLCHAIN=$toolchain TEST_HASH=$test_hash \
                            make -C${appdir} test-murdock
                        RES=$?
                    fi
                fi
            fi
        fi
    fi

    if [ -d ${BINDIR} ]
    then
        echo "-- build directory size: $(du -sh ${BINDIR} | cut -f1)"

        # cleanup
        rm -rf ${BINDIR}
    fi

    return $RES
}

test_job() {
    local appdir=$1
    local board=$(echo $2 | cut -f 1 -d':')
    local toolchain=$(echo $2 | cut -f 2 -d':')

    # interpret any extra arguments as file names.
    # They will be sent along with the job to the test worker
    # and stored in the application's binary folder.
    shift 2
    local files=""
    for filename in "$@"; do
        # check if the file is within $(BINDIR)
        if startswith "${BINDIR}" "${filename}"; then
          # get relative (to $(BINDIR) path
          local relpath="$(realpath --relative-to ${BINDIR} ${filename})"
        else
          error "$0: error: extra test files not within \${BINDIR}!"
        fi

        # set remote file path.
        # currently, the test workers build in the default build path.
        local remote_bindir="${appdir}/bin/${board}"
        files="${files} --file ${filename}:${remote_bindir}/${relpath}"
    done

    dwqc \
        ${DWQ_ENV} \
        ${DWQ_JOBID:+--subjob} \
        --queue ${TEST_QUEUE:-$board} \
        --maxfail 1 \
        $files \
        "./.murdock run_test $appdir $board:$toolchain"
}

run_test() {
    local appdir=$1
    local board=$(echo $2 | cut -f 1 -d':')
    local toolchain=$(echo $2 | cut -f 2 -d':')
    print_worker
    echo "-- executing tests for $appdir on $board (compiled with $toolchain toolchain):"
    hook run_test_pre

    # do flashing and building of termdeps simultaneously
    BOARD=$board TOOLCHAIN=${toolchain} make -C$appdir flash-only termdeps -j2

    # now run the actual test
    if is_in_list "${appdir}" "${TEST_WITH_CONFIG_SUPPORTED}"; then
        BOARD=${board} TOOLCHAIN=${toolchain} make -C${appdir} test-with-config
    else
        BOARD=$board TOOLCHAIN=${toolchain} make -C$appdir test
    fi
    RES=$?

    if [ $RES -eq 0 -a -n "$TEST_HASH" ]; then
        echo -n "-- saving test result to cache: "
        test_cache_put $TEST_HASH
    fi

    return $RES
}

# execute static tests
static_tests() {
    print_worker
    ./dist/tools/ci/static_tests.sh
}

get_non_compile_jobs() {
    [ "$STATIC_TESTS" = "1" ] && \
        echo "$0 static_tests"
}

get_jobs() {
    get_non_compile_jobs
    get_compile_jobs
}

$*