#!/bin/bash

####
# DO NOT EDIT THIS FILE IN MASTER.  ONLY EDIT IT IN THE OLDEST SUPPORTED
# BRANCH, THEN MERGE FORWARD.
####

# This script is used to build Tor for continuous integration.  It should
# be kept the same for all supported Tor versions.
#
# It's subject to the regular Tor license; see LICENSE for copying
# information.

set -o errexit
set -o nounset

# Options for this script.
DEBUG_CI="${DEBUG_CI:-no}"
COLOR_CI="${COLOR_CI:-yes}"

# Options for which CI system this is.
ON_GITLAB="${ON_GITLAB:-yes}"

# Options for things we usually won't want to skip.
RUN_STAGE_CONFIGURE="${RUN_STAGE_CONFIGURE:-yes}"
RUN_STAGE_BUILD="${RUN_STAGE_BUILD:-yes}"
RUN_STAGE_TEST="${RUN_STAGE_TEST:-yes}"

# Options for how to build Tor.  All should be yes/no.
FATAL_WARNINGS="${FATAL_WARNINGS:-yes}"
HARDENING="${HARDENING:-no}"
COVERAGE="${COVERAGE:-no}"
DOXYGEN="${DOXYGEN:-no}"
ASCIIDOC="${ASCIIDOC:-no}"
TRACING="${TRACING:-no}"
ALL_BUGS_ARE_FATAL="${ALL_BUGS_ARE_FATAL:-no}"
DISABLE_DIRAUTH="${DISABLE_DIRAUTH:-no}"
DISABLE_RELAY="${DISABLE_RELAY:-no}"
NSS="${NSS:-no}"
GPL="${GPL:-no}"

# Options for which tests to run.   All should be yes/no.
CHECK="${CHECK:-yes}"
STEM="${STEM:-no}"
CHUTNEY="${CHUTNEY:-no}"
DISTCHECK="${DISTCHECK:-no}"

# Options for where the Tor source is.
CI_SRCDIR="${CI_SRCDIR:-.}"

# Options for where to build.
CI_BUILDDIR="${CI_BUILDDIR:-./build}"

# How parallel should we run make?
MAKE_J_OPT="${MAKE_J_OPT:--j4}"
# Should we stop after make finds an error?
MAKE_K_OPT="${MAKE_K_OPT:--k}"

# What make target should we use for chutney?
CHUTNEY_MAKE_TARGET="${CHUTNEY_MAKE_TARGET:-test-network}"

# Where do we find our additional testing tools?
CHUTNEY_PATH="${CHUTNEY_PATH:-}"
STEM_PATH="${STEM_PATH:-}"

#############################################################################
# Preliminary functions.

# Terminal coloring/emphasis stuff.
if [[ "${COLOR_CI}" == "yes" ]]; then
    T_RED=$(tput setaf 1 || true)
    T_GREEN=$(tput setaf 2 || true)
    T_YELLOW=$(tput setaf 3 || true)
    T_DIM=$(tput dim || true)
    T_BOLD=$(tput bold || true)
    T_RESET=$(tput sgr0 || true)
else
    T_RED=
    T_GREEN=
    T_YELLOW=
    T_DIM=
    T_BOLD=
    T_RESET=
fi

function error()
{
    echo "${T_BOLD}${T_RED}ERROR:${T_RESET} $*" 1>&2
}

function die()
{
    echo "${T_BOLD}${T_RED}FATAL ERROR:${T_RESET} $*" 1>&2
    exit 1
}

function skipping()
{
    echo "${T_BOLD}${T_YELLOW}Skipping $*${T_RESET}"
}

function hooray()
{
    echo "${T_BOLD}${T_GREEN}$*${T_RESET}"
}

if [[ "${DEBUG_CI}" == "yes" ]]; then
    function debug()
    {
        echo "${T_DIM}(debug): $*${T_RESET}"
    }
else
    function debug()
    {
        :
    }
fi

function yes_or_no()
{
    local varname="$1"
    local value="${!varname}"
    debug "${varname} is ${value}"
    if [[ "${value}" != 'yes' && "${value}" != 'no' ]]; then
        die "${varname} must be 'yes' or 'no'.  Got unexpected value ${value}".
    fi
}

function incompatible()
{
    local varname1="$1"
    local varname2="$2"
    local val1="${!varname1}"
    local val2="${!varname2}"
    if [[ "${val1}" = 'yes' && "${val2}" = 'yes' ]]; then
        die "Cannot set both ${varname1} and ${varname2}: they are incompatible."
    fi
}

function runcmd()
{
    echo "${T_BOLD}\$ $*${T_RESET}"
    if ! "$@" ; then
        error "command '$*' has failed."
        return 1
    fi
}

function show_git_version()
{
    local tool="$1"
    local dir="$2"
    local version="?????"
    if [[ -e "$dir/.git" ]] ; then
        version=$(cd "$dir"; git rev-parse HEAD)
    fi
    echo "${T_BOLD}$tool:${T_RESET} $version"
}

if [[ "${ON_GITLAB}" == "yes" ]]; then
    function start_section()
    {
        local label="$1"
        local stamp
        stamp=$(date +%s)
        printf "section_start:%s:%s\r\e[0K" "$stamp" "$label"
        echo "${T_BOLD}${T_GREEN}========= $label${T_RESET}"
    }
    function end_section()
    {
        local label="$1"
        local stamp
        stamp=$(date +%s)
        printf "section_end:%s:%s\r\e[0K" "$stamp" "$label"
    }
else
    function start_section()
    {
        true
    }
    function end_section()
    {
        true
    }
fi

#############################################################################
# Validate inputs.

debug Validating inputs
yes_or_no DEBUG_CI
yes_or_no COLOR_CI
yes_or_no ON_GITLAB
yes_or_no FATAL_WARNINGS
yes_or_no HARDENING
yes_or_no COVERAGE
yes_or_no DOXYGEN
yes_or_no ASCIIDOC
yes_or_no TRACING
yes_or_no ALL_BUGS_ARE_FATAL
yes_or_no DISABLE_DIRAUTH
yes_or_no DISABLE_RELAY
yes_or_no NSS
yes_or_no GPL

yes_or_no RUN_STAGE_CONFIGURE
yes_or_no RUN_STAGE_BUILD
yes_or_no RUN_STAGE_TEST

yes_or_no CHECK
yes_or_no STEM
yes_or_no DISTCHECK

incompatible DISTCHECK CHECK
incompatible DISTCHECK CHUTNEY
incompatible DISTCHECK STEM
incompatible DISTCHECK COVERAGE
incompatible DISTCHECK DOXYGEN

if [[ "${CHUTNEY}" = yes && "${CHUTNEY_PATH}" = '' ]] ; then
    die "CHUTNEY is set to 'yes', but CHUTNEY_PATH was not specified."
fi

if [[ "${STEM}" = yes && "${STEM_PATH}" = '' ]] ; then
    die "STEM is set to 'yes', but STEM_PATH was not specified."
fi

#############################################################################
# Set up options for make and configure.

make_options=()
if [[ "$MAKE_J_OPT" != "" ]]; then
    make_options+=("$MAKE_J_OPT")
fi
if [[ "$MAKE_K_OPT" != "" ]]; then
    make_options+=("$MAKE_K_OPT")
fi

configure_options=()
if [[ "$FATAL_WARNINGS" == "yes" ]]; then
    configure_options+=("--enable-fatal-warnings")
fi
if [[ "$HARDENING" == "yes" ]]; then
    configure_options+=("--enable-fragile-hardening")
fi
if [[ "$COVERAGE" == "yes" ]]; then
    configure_options+=("--enable-coverage")
fi
if [[ "$ASCIIDOC" != "yes" ]]; then
    configure_options+=("--disable-asciidoc")
fi
if [[ "$TRACING" == "yes" ]]; then
    configure_options+=("--enable-tracing-instrumentation-lttng")
fi
if [[ "$ALL_BUGS_ARE_FATAL" == "yes" ]]; then
    configure_options+=("--enable-all-bugs-are-fatal")
fi
if [[ "$DISABLE_DIRAUTH" == "yes" ]]; then
    configure_options+=("--disable-module-dirauth")
fi
if [[ "$DISABLE_RELAY" == "yes" ]]; then
    configure_options+=("--disable-module-relay")
fi
if [[ "$NSS" == "yes" ]]; then
    configure_options+=("--enable-nss")
fi
if [[ "$GPL" == "yes" ]]; then
    configure_options+=("--enable-gpl")
fi

#############################################################################
# Tell the user about our versions of different tools and packages.

uname -a
printf "python: "
python -V || echo "no 'python' binary."
printf "python3: "
python3 -V || echo "no 'python3' binary."

show_git_version Tor "${CI_SRCDIR}"
if [[ "${STEM}" = "yes" ]]; then
    show_git_version Stem "${STEM_PATH}"
fi
if [[ "${CHUTNEY}" = "yes" ]]; then
    show_git_version Chutney "${CHUTNEY_PATH}"
fi

#############################################################################
# Determine the version of Tor.

TOR_VERSION=$(grep -m 1 AC_INIT "${CI_SRCDIR}"/configure.ac | sed -e 's/.*\[//; s/\].*//;')

# Use variables like these when we need to behave differently depending on
# Tor version.  Only create the variables we need.
TOR_VER_AT_LEAST_043=no
TOR_VER_AT_LEAST_044=no

# These are the currently supported Tor versions; no need to work with anything
# ancient in this script.
case "$TOR_VERSION" in
    0.4.7.*)
        TOR_VER_AT_LEAST_043=yes
        TOR_VER_AT_LEAST_044=yes
        ;;
    0.4.8.*)
        TOR_VER_AT_LEAST_043=yes
        TOR_VER_AT_LEAST_044=yes
        ;;
esac

#############################################################################
# Make sure the directories are all there.

# Make sure CI_SRCDIR exists and has a file we expect.
if [[ ! -d "$CI_SRCDIR" ]] ; then
    die "CI_SRCDIR=${CI_SRCDIR} is not a directory"
fi
if [[ ! -f "$CI_SRCDIR/src/core/or/or.h" ]] ; then
    die "CI_SRCDIR=${CI_SRCDIR} does not look like a Tor directory."
fi

# Make CI_SRCDIR absolute.
CI_SRCDIR=$(cd "$CI_SRCDIR" && pwd)

# Create an "artifacts" directory to copy artifacts into.
mkdir -p ./artifacts

if [[ "$RUN_STAGE_CONFIGURE" = "yes" ]]; then

    start_section "Autogen"
    runcmd cd "${CI_SRCDIR}"
    runcmd ./autogen.sh
    runcmd mkdir -p "${CI_BUILDDIR}"
    runcmd cd "${CI_BUILDDIR}"
    end_section "Autogen"

    # make the builddir absolute too.
    CI_BUILDDIR=$(pwd)

    start_section "Configure"
    if ! runcmd "${CI_SRCDIR}"/configure "${configure_options[@]}" ; then
        error "Here is the end of config.log:"
        runcmd tail config.log
        die "Unable to continue"
    fi
    end_section "Configure"
else
    debug "Skipping configure stage. Making sure that ${CI_BUILDDIR}/config.log exists."
    if [[ ! -d "${CI_BUILDDIR}" ]]; then
        die "Build directory ${CI_BUILDDIR} did not exist!"
    fi
    if [[ ! -f "${CI_BUILDDIR}/config.log" ]]; then
        die "Tor was not configured in ${CI_BUILDDIR}!"
    fi

    cp config.log "${CI_SRCDIR}"/artifacts

    runcmd cd "${CI_BUILDDIR}"
    CI_BUILDDIR=$(pwd)
fi

###############################
# Build Tor.

if [[ "$RUN_STAGE_BUILD" = "yes" ]] ; then
    if [[ "$DISTCHECK" = "no" ]]; then
        start_section "Build"
        runcmd make "${make_options[@]}" all
        cp src/app/tor "${CI_SRCDIR}"/artifacts
        end_section "Build"
    else
        export DISTCHECK_CONFIGURE_FLAGS="${configure_options[*]}"
        # XXXX Set make options?
        start_section Distcheck
        if runcmd make "${make_options[@]}" distcheck ; then
            hooray "Distcheck was successful. Nothing further will be done."
            # We have to exit early here, since we can't do any other tests.
            cp tor-*.tar.gz "${CI_SRCDIR}"/artifacts
        else
            error "Diagnostics:"
            runcmd make show-distdir-testlog || true
            runcmd make show-distdir-core || true
            die "Unable to continue."
        fi
        end_section Distcheck
        exit 0
    fi
fi

##############################
# Run tests.

if [[ "$RUN_STAGE_TEST" == "no" ]]; then
    echo "Skipping tests. Exiting now."
    exit 0
fi

FAILED_TESTS=""

if [[ "${DOXYGEN}" = 'yes' ]]; then
    start_section Doxygen
    if [[ "${TOR_VER_AT_LEAST_043}" = 'yes' ]]; then
        if runcmd make doxygen; then
            hooray "make doxygen has succeeded."
        else
            FAILED_TESTS="${FAILED_TESTS} doxygen"
        fi
    else
        skipping "make doxygen: doxygen is broken for Tor < 0.4.3"
    fi
    end_section Doxygen
fi

if [[ "${ASCIIDOC}" = 'yes' ]]; then
    start_section Asciidoc
    if runcmd make manpages; then
        hooray "make manpages has succeeded."
    else
        FAILED_TESTS="${FAILED_TESTS} asciidoc"
    fi
    end_section Asciidoc
fi

if [[ "${CHECK}" = "yes" ]]; then
    start_section "Check"
    if runcmd make "${make_options[@]}" check; then
        hooray "make check has succeeded."
    else
        error "Here are the contents of the test suite output:"
        runcmd cat test-suite.log || true
        FAILED_TESTS="${FAILED_TESTS} check"
    fi
    end_section "Check"
fi

if [[ "${CHUTNEY}" = "yes" ]]; then
    start_section "Chutney"
    export CHUTNEY_TOR_SANDBOX=0
    export CHUTNEY_ALLOW_FAILURES=2
    # Send 5MB for every verify check.
    export CHUTNEY_DATA_BYTES=5000000
    if runcmd make "${CHUTNEY_MAKE_TARGET}"; then
        hooray "Chutney tests have succeeded"
    else
        error "Chutney says:"
        export CHUTNEY_DATA_DIR="${CHUTNEY_PATH}/net"
        runcmd "${CHUTNEY_PATH}"/tools/diagnostics.sh || true
        # XXXX These next two should be part of a make target.
        runcmd ls test_network_log || true
        runcmd head -n -0 test_network_log/* || true
        FAILED_TESTS="${FAILED_TESTS} chutney"
    fi
    end_section "Chutney"
fi

if [[ "${STEM}" = "yes" ]]; then
    start_section "Stem"
    # 0.3.5 and onward have now disabled onion service v2 so we need to exclude
    # these Stem tests from now on.
    EXCLUDE_TESTS="--exclude-test control.controller.test_ephemeral_hidden_services_v2 --exclude-test control.controller.test_hidden_services_conf --exclude-test control.controller.test_with_ephemeral_hidden_services_basic_auth --exclude-test control.controller.test_without_ephemeral_hidden_services --exclude-test control.controller.test_with_ephemeral_hidden_services_basic_auth_no_credentials --exclude-test control.controller.test_with_detached_ephemeral_hidden_services --exclude-test control.controller.test_with_invalid_ephemeral_hidden_service_port --exclude-test control.controller.test_ephemeral_hidden_services_v3"
    if [[ "${TOR_VER_AT_LEAST_044}" = 'yes' ]]; then
        # XXXX This should probably be part of some test-stem make target.

        # Disable the check around EXCLUDE_TESTS that requires double quote. We
        # need it to be expanded.
        # shellcheck disable=SC2086
        if runcmd timelimit -p -t 520 -s USR1 -T 30 -S ABRT \
            python3 "${STEM_PATH}/run_tests.py" \
            --tor src/app/tor \
            --integ --test control.controller \
            $EXCLUDE_TESTS \
            --test control.base_controller \
            --test process \
            --log TRACE \
            --log-file stem.log ; then
            hooray "Stem tests have succeeded"
        else
            error "Stem output:"
            runcmd tail -1000 "${STEM_PATH}"/test/data/tor_log
            runcmd grep -v "SocketClosed" stem.log | tail -1000
            FAILED_TESTS="${FAILED_TESTS} stem"
        fi
    else
        skipping "Stem: broken with <= 0.4.3. See bug tor#40077"
    fi
    end_section "Stem"
fi

# TODO: Coverage

if [[ "${FAILED_TESTS}" != "" ]]; then
    die "Failed tests: ${FAILED_TESTS}"
fi

hooray "Everything seems fine."
