#!/bin/bash
# ---------------------------------------------------------------------------
# kselftest engine.
#
# The selftests are built and shipped by the kernel-selftests-internal RPM,
# so nothing is compiled here either.  Red Hat also ships prepare_system.sh,
# which sets up whatever a collection needs, in three escalating tiers.
#
#     ./tests/run-kselftest.sh <run-dir>
# ---------------------------------------------------------------------------
set -uo pipefail

KT_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
# shellcheck source=../lib/common.sh
. "$KT_ROOT/lib/common.sh"
kt_load_settings

RUN_DIR=${1:-}
[ -n "$RUN_DIR" ] || kt_die "usage: run-kselftest.sh <run-dir>"
mkdir -p "$RUN_DIR/kselftest"
CSV="$RUN_DIR/results.csv"
LOG="$RUN_DIR/kselftest/kselftest.log"

kt_head "kselftest"

# --- preflight -------------------------------------------------------------
RUNNER="$KSELFTEST_DIR/run_kselftest.sh"
LIST="$KSELFTEST_DIR/kselftest-list.txt"
if [ ! -x "$RUNNER" ] || [ ! -r "$LIST" ]; then
    kt_warn "kselftest not found under $KSELFTEST_DIR"
    kt_warn "Install the selftests package built for this kernel:"
    kt_warn "    dnf install kernel-selftests-internal-\$(uname -r | sed 's/\\.x86_64\$//')"
    kt_record "$CSV" kselftest "" "(engine)" SKIP "" "kernel-selftests-internal not installed"
    exit 0
fi

# The shipped kselftest-list.txt starts with a few lines of build log
# ("Auto-detecting system features:", a colourised llvm: [ OFF ] line and
# blank lines).  run_kselftest.sh would take those for collection names, so
# only well formed "collection:test" entries are kept.
VALID_LIST="$RUN_DIR/kselftest/tests.txt"
grep -E '^[A-Za-z0-9_./-]+:[^[:space:]]+$' "$LIST" > "$VALID_LIST"
junk=$(( $(wc -l < "$LIST") - $(wc -l < "$VALID_LIST") ))
[ "$junk" -gt 0 ] && kt_log "ignored $junk non-test lines in kselftest-list.txt"

# --- which collections -----------------------------------------------------
if [ -n "${KSELFTEST_COLLECTIONS:-}" ]; then
    read -r -a WANT <<< "$KSELFTEST_COLLECTIONS"
else
    plist="$KT_ROOT/conf/kselftest-${KT_PROFILE}.list"
    [ -r "$plist" ] || kt_die "no collection list for profile '$KT_PROFILE' ($plist)"
    mapfile -t WANT < <(grep -vE '^\s*(#|$)' "$plist")
fi

ALL_COLLECTIONS=$(cut -d: -f1 "$VALID_LIST" | sort -u)
if [ "${WANT[0]:-}" = all ]; then
    mapfile -t WANT <<< "$ALL_COLLECTIONS"
fi

# --- build the test list ---------------------------------------------------
DENY="$KT_ROOT/conf/kselftest-deny.list"
is_denied() {
    [ -r "$DENY" ] || return 1
    grep -qxF "$1" <(grep -vE '^\s*(#|$)' "$DENY")
}

TESTS=()
for c in "${WANT[@]}"; do
    [ -n "$c" ] || continue
    if ! grep -q "^$c:" "$VALID_LIST"; then
        kt_warn "no such collection: $c"
        kt_record "$CSV" kselftest "$c" "(collection)" SKIP "" "collection not present in this build"
        continue
    fi
    while IFS= read -r entry; do
        # Denied tests only run under "full" AND KT_SAFE=0.  Gating on
        # KT_SAFE alone would let "--profile standard --unsafe" run them,
        # on a profile that is documented as leaving the machine alone.
        if { [ "$KT_SAFE" = 1 ] || [ "$KT_PROFILE" != full ]; } && is_denied "$entry"; then
            kt_record "$CSV" kselftest "${entry%%:*}" "${entry#*:}" SKIP "" "on kselftest-deny.list"
            continue
        fi
        TESTS+=("$entry")
    done < <(grep "^$c:" "$VALID_LIST")
done

if [ ${#TESTS[@]} -eq 0 ]; then
    kt_warn "no kselftests selected"
    exit 0
fi
kt_log "${#TESTS[@]} selftests from ${#WANT[@]} collections"

# --- prepare the system ----------------------------------------------------
# -s safe, -m also loads modules, -d also makes destructive changes.
PREP="$KSELFTEST_DIR/prepare_system.sh"
if [ -x "$PREP" ]; then
    if [ "$KT_PROFILE" = full ] && [ "$KT_SAFE" = 0 ]; then
        prep_flag=-d
    elif [ "$KT_PROFILE" = full ]; then
        prep_flag=-m
    else
        prep_flag=-s
    fi
    kt_log "prepare_system.sh $prep_flag (tier: $prep_flag)"
    for c in "${WANT[@]}"; do
        "$PREP" "$prep_flag" "$c" >> "$LOG" 2>&1 || \
            kt_warn "prepare_system.sh $prep_flag $c returned non-zero (continuing)"
    done
else
    kt_warn "prepare_system.sh not shipped; some collections may report failures they would not otherwise"
fi

# --- run -------------------------------------------------------------------
# One test per invocation.  Running a whole collection in one go is faster,
# but a single hang then costs the whole collection and there is no way to
# give one test its own timeout.
mark_all=$(kt_dmesg_mark)
n=0
for entry in "${TESTS[@]}"; do
    coll=${entry%%:*}
    name=${entry#*:}
    n=$(( n + 1 ))
    printf '  [%3d/%3d] %-46s' "$n" "${#TESTS[@]}" "$entry"

    out="$RUN_DIR/kselftest/$(printf '%s' "$entry" | tr '/:' '__').out"
    mark=$(kt_dmesg_mark)
    start=$(date +%s)
    # -o is kselftest's own per-test timeout.  The outer timeout is the
    # backstop for a test that ignores it, and is given more room so the
    # inner one gets the chance to report first.
    outer=$(( KSELFTEST_TIMEOUT + 60 ))
    ( cd "$KSELFTEST_DIR" && timeout --kill-after=15s "${outer}s" \
        "$RUNNER" -t "$entry" -o "$KSELFTEST_TIMEOUT" ) > "$out" 2>&1
    rc=$?
    dur=$(( $(date +%s) - start ))

    rows=$("$KT_PYTHON" "$KT_ROOT/lib/parse_results.py" kselftest "$out")
    if [ -n "$rows" ]; then
        # The parser leaves duration empty; fill in what we measured.
        printf '%s\n' "$rows" | awk -F'","' -v d="$dur" 'BEGIN{OFS="\",\""}
            { $5 = d; print }' >> "$CSV"
        status=$(printf '%s\n' "$rows" | awk -F'","' '{print $4}' | sort -u | tr '\n' ' ')
    elif [ "$rc" = 124 ] || [ "$rc" = 137 ]; then
        kt_record "$CSV" kselftest "$coll" "$name" TIMEOUT "$dur" "killed after ${outer}s"
        status=TIMEOUT
    else
        kt_record "$CSV" kselftest "$coll" "$name" ERROR "$dur" "no TAP output, rc=$rc"
        status=ERROR
    fi

    case $status in
        *FAIL*|*ERROR*|*TIMEOUT*) printf '%s%s%s\n' "$C_RED" "$status" "$C_OFF" ;;
        *SKIP*)                   printf '%s%s%s\n' "$C_DIM" "$status" "$C_OFF" ;;
        *)                        printf '%s%s%s\n' "$C_GRN" "$status" "$C_OFF" ;;
    esac

    if kt_scan_dmesg "$mark" "$out.badness"; then
        kt_warn "    kernel log complained during $entry"
    else
        rm -f "$out.badness"
    fi

    if [ "$KT_FAIL_FAST" = 1 ] && [[ $status == *FAIL* ]]; then
        kt_err "KT_FAIL_FAST is set; stopping after $entry"
        break
    fi
done

kt_dmesg_since "$mark_all" > "$RUN_DIR/kselftest/dmesg.txt"
exit 0
