Skip to main content

priv/scripts/runner_self_check.sh

#!/usr/bin/env bash

# runner_self_check.sh
#
# Local, Docker-free verification of every TESTCASE that
# `priv/interop/run_endpoint.sh` accepts. For each testcase we:
#
#   1. Start an nquic interop server with `TESTCASE=<name>` so that
#      `interop_server:apply_testcase_opts/2` configures the server-side
#      knobs (e.g. retry=true, cipher_suites=[chacha20_poly1305]).
#   2. Drive `interop_client:run_endpoint/5` against it (the same
#      Erlang function `run_endpoint.sh` calls), so the client-side
#      wiring (cipher list, key update trigger, session-cache
#      resumption) is exercised end-to-end.
#   3. Capture the client exit code and report PASS/FAIL.
#
# This is not a substitute for the official quic-interop-runner; it
# does not inspect qlog or pcap, so it cannot prove that, e.g., a Retry
# packet was actually emitted. It does prove that the handler the
# runner invokes produces a successful end-to-end exchange.
#
# Usage:
#   ./priv/scripts/runner_self_check.sh                  # all testcases
#   ./priv/scripts/runner_self_check.sh retry resumption # just these

set -u

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$DIR/../.."
LOG_DIR="$PROJECT_ROOT/test/compliance_logs/runner_self"
WWW_DIR="$PROJECT_ROOT/test/interop/www"
PORT_BASE=44330

mkdir -p "$LOG_DIR"

# Default test matrix: every TESTCASE that run_endpoint.sh accepts as
# supported. Keep this list in sync with the case statement in
# priv/interop/run_endpoint.sh.
ALL_TESTS=(handshake transfer multiconnect retry chacha20 keyupdate resumption zerortt)

if [ $# -gt 0 ]; then
    TESTS=("$@")
else
    TESTS=("${ALL_TESTS[@]}")
fi

cd "$PROJECT_ROOT"

echo "[*] Building nquic (interop profile)..."
if ! rebar3 as interop compile > "$LOG_DIR/compile.log" 2>&1; then
    echo "    Build failed (see $LOG_DIR/compile.log)"
    exit 1
fi

# Per-testcase resources. The server is restarted each time so its
# TESTCASE-driven opts apply.
SERVER_PID=""
cleanup() {
    if [ -n "$SERVER_PID" ]; then
        kill -TERM "$SERVER_PID" 2>/dev/null || true
        wait "$SERVER_PID" 2>/dev/null || true
        SERVER_PID=""
    fi
    pkill -f "nquic_runner_self_(srv|cli)_" 2>/dev/null || true
}
trap cleanup EXIT INT TERM

start_server() {
    local TC="$1"
    local SERVER_PORT="$2"
    local LOG_FILE="$LOG_DIR/server_${TC}.log"

    # `tail -f /dev/null` keeps stdin open so the rebar3 shell doesn't
    # exit on EOF; without it, the listener `accept_loop` recursion
    # never gets a chance to run before the shell tears down.
    tail -f /dev/null | \
    PORT="$SERVER_PORT" \
    WWWDIR="$WWW_DIR" \
    TESTCASE="$TC" \
    rebar3 as interop shell \
        --name "nquic_runner_self_srv_${TC}_$$@127.0.0.1" \
        --eval 'interop_server:start()' \
        > "$LOG_FILE" 2>&1 &
    SERVER_PID=$!

    # Poll for "Listening" line; bail out after ~10 s.
    for _ in $(seq 1 100); do
        if grep -q "Listening on" "$LOG_FILE" 2>/dev/null; then
            return 0
        fi
        if ! kill -0 "$SERVER_PID" 2>/dev/null; then
            echo "    server died before listening (see $LOG_FILE)"
            SERVER_PID=""
            return 1
        fi
        sleep 0.1
    done
    echo "    server did not become ready within 10 s (see $LOG_FILE)"
    return 1
}

stop_server() {
    if [ -n "$SERVER_PID" ]; then
        kill -TERM "$SERVER_PID" 2>/dev/null || true
        wait "$SERVER_PID" 2>/dev/null || true
        SERVER_PID=""
    fi
}

# Pick the request set for a testcase. Resumption needs >=2 URLs so the
# second connection can prove ticket reuse; multiconnect benefits from
# more than one path so the loop has something to do per iteration.
paths_for() {
    local TC="$1"
    case "$TC" in
        resumption|multiconnect)
            # Resumption needs >= 2 connections so the second one can
            # prove the cached ticket is reused. medium.bin (1 MB)
            # exercises the per-stream send-side buffering + cwnd-aware
            # drain path; a small.txt by itself does not.
            echo '"/small.txt", "/medium.bin"'
            ;;
        transfer)
            # The bulk-transfer testcase fetches a single object;
            # use medium.bin so the harness exercises STREAM-frame
            # splitting and ACK-driven re-flush.
            echo '"/medium.bin"'
            ;;
        *)
            echo '"/small.txt"'
            ;;
    esac
}

run_client() {
    local TC="$1"
    local SERVER_PORT="$2"
    local LOG_FILE="$LOG_DIR/client_${TC}.log"
    local DOWNLOADS_DIR="$LOG_DIR/downloads_${TC}"

    rm -rf "$DOWNLOADS_DIR"
    mkdir -p "$DOWNLOADS_DIR"

    local PATHS
    PATHS=$(paths_for "$TC")

    # Build the -pa argument list once; interop_client/server live
    # under test/interop and the ctx owner-loop driver under
    # test/support in the interop profile.
    local PA_ARGS=""
    for d in "$PROJECT_ROOT"/_build/interop/lib/*/ebin; do
        [ -d "$d" ] && PA_ARGS="$PA_ARGS -pa $d"
    done
    for d in "$PROJECT_ROOT"/_build/interop/lib/*/test/interop \
             "$PROJECT_ROOT"/_build/interop/lib/*/test/support; do
        [ -d "$d" ] && PA_ARGS="$PA_ARGS -pa $d"
    done

    # Use plain `erl -noshell` rather than `rebar3 shell --eval` so the
    # `halt(Code)` exit code actually reaches the script. rebar3 wraps
    # the shell and always exits 0, which would mask every failure.
    local EVAL="
        Paths = [${PATHS}],
        Code = interop_client:run_endpoint(
            \"127.0.0.1\", ${SERVER_PORT}, Paths, \"${DOWNLOADS_DIR}\", \"${TC}\"
        ),
        halt(Code)
    "

    erl -noshell \
        -name "nquic_runner_self_cli_${TC}_$$@127.0.0.1" \
        $PA_ARGS \
        -eval "$EVAL" \
        > "$LOG_FILE" 2>&1
    return $?
}

PASS=0
FAIL=0

echo ""
echo "==========================================="
echo "   run_endpoint.sh testcase self-check"
echo "==========================================="
echo ""

i=0
for TC in "${TESTS[@]}"; do
    i=$((i + 1))
    TEST_PORT=$((PORT_BASE + i))
    printf "  %-15s " "$TC"

    if ! start_server "$TC" "$TEST_PORT"; then
        echo "FAIL (server)"
        FAIL=$((FAIL + 1))
        continue
    fi

    if run_client "$TC" "$TEST_PORT"; then
        echo "PASS"
        PASS=$((PASS + 1))
    else
        echo "FAIL (see $LOG_DIR/client_${TC}.log)"
        FAIL=$((FAIL + 1))
    fi

    stop_server
done

echo ""
echo "Results: $PASS PASS, $FAIL FAIL"
echo "Logs:    $LOG_DIR"

[ "$FAIL" -eq 0 ]