#!/usr/bin/env bash
#
# Copyright (c) 2019-2020 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# Test for deterministic coverage across unit test runs.

export LC_ALL=C

# Use GCOV_EXECUTABLE="gcov" if compiling with gcc.
# Use GCOV_EXECUTABLE="llvm-cov gcov" if compiling with clang.
GCOV_EXECUTABLE="gcov"

# Disable tests known to cause non-deterministic behaviour and document the source or point of non-determinism.
NON_DETERMINISTIC_TESTS=(
    "blockfilter_index_tests/blockfilter_index_initial_sync"  # src/checkqueue.h: In CCheckQueue::Loop(): while (queue.empty()) { ... }
    "coinselector_tests/knapsack_solver_test"                 # coinselector_tests.cpp: if (equal_sets(setCoinsRet, setCoinsRet2))
    "fs_tests/fsbridge_fstream"                               # deterministic test failure?
    "miner_tests/CreateNewBlock_validity"                     # validation.cpp: if (signals.CallbacksPending() > 10)
    "scheduler_tests/manythreads"                             # scheduler.cpp: CScheduler::serviceQueue()
    "scheduler_tests/singlethreadedscheduler_ordered"         # scheduler.cpp: CScheduler::serviceQueue()
    "txvalidationcache_tests/checkinputs_test"                # validation.cpp: if (signals.CallbacksPending() > 10)
    "txvalidationcache_tests/tx_mempool_block_doublespend"    # validation.cpp: if (signals.CallbacksPending() > 10)
    "txindex_tests/txindex_initial_sync"                      # validation.cpp: if (signals.CallbacksPending() > 10)
    "txvalidation_tests/tx_mempool_reject_coinbase"           # validation.cpp: if (signals.CallbacksPending() > 10)
    "validation_block_tests/processnewblock_signals_ordering" # validation.cpp: if (signals.CallbacksPending() > 10)
    "wallet_tests/coin_mark_dirty_immature_credit"            # validation.cpp: if (signals.CallbacksPending() > 10)
    "wallet_tests/dummy_input_size_test"                      # validation.cpp: if (signals.CallbacksPending() > 10)
    "wallet_tests/importmulti_rescan"                         # validation.cpp: if (signals.CallbacksPending() > 10)
    "wallet_tests/importwallet_rescan"                        # validation.cpp: if (signals.CallbacksPending() > 10)
    "wallet_tests/ListCoins"                                  # validation.cpp: if (signals.CallbacksPending() > 10)
    "wallet_tests/scan_for_wallet_transactions"               # validation.cpp: if (signals.CallbacksPending() > 10)
    "wallet_tests/wallet_disableprivkeys"                     # validation.cpp: if (signals.CallbacksPending() > 10)
)

TEST_BITCOIN_BINARY="src/test/test_bitcoin"

print_usage() {
    echo "Usage: $0 [custom test filter (default: all but known non-deterministic tests)] [number of test runs (default: 2)]"
}

N_TEST_RUNS=2
BOOST_TEST_RUN_FILTERS=""
if [[ $# != 0 ]]; then
    if [[ $1 == "--help" ]]; then
        print_usage
        exit
    fi
    PARSED_ARGUMENTS=0
    if [[ $1 =~ [a-z] ]]; then
        BOOST_TEST_RUN_FILTERS=$1
        PARSED_ARGUMENTS=$((PARSED_ARGUMENTS + 1))
        shift
    fi
    if [[ $1 =~ ^[0-9]+$ ]]; then
        N_TEST_RUNS=$1
        PARSED_ARGUMENTS=$((PARSED_ARGUMENTS + 1))
        shift
    fi
    if [[ ${PARSED_ARGUMENTS} == 0 || $# -gt 2 || ${N_TEST_RUNS} -lt 2 ]]; then
        print_usage
        exit
    fi
fi
if [[ ${BOOST_TEST_RUN_FILTERS} == "" ]]; then
    BOOST_TEST_RUN_FILTERS="$(IFS=":"; echo "!${NON_DETERMINISTIC_TESTS[*]}" | sed 's/:/:!/g')"
else
    echo "Using Boost test filter: ${BOOST_TEST_RUN_FILTERS}"
    echo
fi

if ! command -v gcov > /dev/null; then
    echo "Error: gcov not installed. Exiting."
    exit 1
fi

if ! command -v gcovr > /dev/null; then
    echo "Error: gcovr not installed. Exiting."
    exit 1
fi

if [[ ! -e ${TEST_BITCOIN_BINARY} ]]; then
    echo "Error: Executable ${TEST_BITCOIN_BINARY} not found. Run \"cmake -B build -DCMAKE_BUILD_TYPE=Coverage\" and compile."
    exit 1
fi

get_file_suffix_count() {
    find src/ -type f -name "*.$1" | wc -l
}

if [[ $(get_file_suffix_count gcno) == 0 ]]; then
    echo "Error: Could not find any *.gcno files. The *.gcno files are generated by the compiler. Run \"cmake -B build -DCMAKE_BUILD_TYPE=Coverage\" and re-compile."
    exit 1
fi

get_covr_filename() {
    echo "gcovr.run-$1.txt"
}

TEST_RUN_ID=0
while [[ ${TEST_RUN_ID} -lt ${N_TEST_RUNS} ]]; do
    TEST_RUN_ID=$((TEST_RUN_ID + 1))
    echo "[$(date +"%Y-%m-%d %H:%M:%S")] Measuring coverage, run #${TEST_RUN_ID} of ${N_TEST_RUNS}"
    find src/ -type f -name "*.gcda" -exec rm {} \;
    if [[ $(get_file_suffix_count gcda) != 0 ]]; then
        echo "Error: Stale *.gcda files found. Exiting."
        exit 1
    fi
    TEST_OUTPUT_TEMPFILE=$(mktemp)
    if ! BOOST_TEST_RUN_FILTERS="${BOOST_TEST_RUN_FILTERS}" ${TEST_BITCOIN_BINARY} > "${TEST_OUTPUT_TEMPFILE}" 2>&1; then
        cat "${TEST_OUTPUT_TEMPFILE}"
        rm "${TEST_OUTPUT_TEMPFILE}"
        exit 1
    fi
    rm "${TEST_OUTPUT_TEMPFILE}"
    if [[ $(get_file_suffix_count gcda) == 0 ]]; then
        echo "Error: Running the test suite did not create any *.gcda files. The gcda files are generated when the instrumented test programs are executed. Run \"cmake -B build -DCMAKE_BUILD_TYPE=Coverage\" and re-compile."
        exit 1
    fi
    GCOVR_TEMPFILE=$(mktemp)
    if ! gcovr --gcov-executable "${GCOV_EXECUTABLE}" -r src/ > "${GCOVR_TEMPFILE}"; then
        echo "Error: gcovr failed. Output written to ${GCOVR_TEMPFILE}. Exiting."
        exit 1
    fi
    GCOVR_FILENAME=$(get_covr_filename ${TEST_RUN_ID})
    mv "${GCOVR_TEMPFILE}" "${GCOVR_FILENAME}"
    if grep -E "^TOTAL *0 *0 " "${GCOVR_FILENAME}"; then
        echo "Error: Spurious gcovr output. Make sure the correct GCOV_EXECUTABLE variable is set in $0 (\"gcov\" for gcc, \"llvm-cov gcov\" for clang)."
        exit 1
    fi
    if [[ ${TEST_RUN_ID} != 1 ]]; then
        COVERAGE_DIFF=$(diff -u "$(get_covr_filename 1)" "${GCOVR_FILENAME}")
        if [[ ${COVERAGE_DIFF} != "" ]]; then
            echo
            echo "The line coverage is non-deterministic between runs. Exiting."
            echo
            echo "The test suite must be deterministic in the sense that the set of lines executed at least"
            echo "once must be identical between runs. This is a necessary condition for meaningful"
            echo "coverage measuring."
            echo
            echo "${COVERAGE_DIFF}"
            exit 1
        fi
        rm "${GCOVR_FILENAME}"
    fi
done

echo
echo "Coverage test passed: Deterministic coverage across ${N_TEST_RUNS} runs."
exit
