# shellcheck shell=bash
# ---------------------------------------------------------------------------
# Helpers shared by test-kernel.sh, test-patch.sh and the tests/ runners.
# Sourced, never executed.
# ---------------------------------------------------------------------------

KT_ROOT=${KT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}
export KT_ROOT

# Pin the system python.  A venv earlier in PATH (semgrep, ollama, ...) would
# otherwise be used and its byte code is not portable to the target host.
KT_PYTHON=${KT_PYTHON:-/usr/bin/python3}

# --- output ----------------------------------------------------------------
if [ -t 1 ] && [ "${KT_NO_COLOR:-0}" != 1 ]; then
    C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'
    C_BLU=$'\033[34m'; C_DIM=$'\033[2m';  C_OFF=$'\033[0m'
else
    C_RED=; C_GRN=; C_YEL=; C_BLU=; C_DIM=; C_OFF=
fi

kt_log()  { printf '%s %s\n'    "$(date '+%H:%M:%S')" "$*"; }
kt_info() { printf '%s %s%s%s\n' "$(date '+%H:%M:%S')" "$C_BLU" "$*" "$C_OFF"; }
kt_ok()   { printf '%s %s%s%s\n' "$(date '+%H:%M:%S')" "$C_GRN" "$*" "$C_OFF"; }
kt_warn() { printf '%s %s%s%s\n' "$(date '+%H:%M:%S')" "$C_YEL" "$*" "$C_OFF" >&2; }
kt_err()  { printf '%s %s%s%s\n' "$(date '+%H:%M:%S')" "$C_RED" "$*" "$C_OFF" >&2; }
kt_die()  { kt_err "$*"; exit 1; }

kt_head() {
    printf '\n%s== %s ==%s\n' "$C_BLU" "$*" "$C_OFF"
}

# --- settings --------------------------------------------------------------
# Load settings.conf, but never clobber a value already set in the environment.
kt_load_settings() {
    local conf=${1:-$KT_ROOT/settings.conf}
    [ -r "$conf" ] || kt_die "settings file not found: $conf"
    local line key val
    while IFS= read -r line; do
        case $line in ''|\#*) continue ;; esac
        key=${line%%=*}
        val=${line#*=}
        key=${key// /}
        [ -n "$key" ] || continue
        # Strip one layer of surrounding quotes.
        case $val in
            \"*\") val=${val#\"}; val=${val%\"} ;;
            \'*\') val=${val#\'}; val=${val%\'} ;;
        esac
        if [ -z "${!key+set}" ]; then
            printf -v "$key" '%s' "$val"
            export "${key?}"
        fi
    done < "$conf"
}

# --- environment facts -----------------------------------------------------
kt_kver()    { uname -r; }
kt_arch()    { uname -m; }
kt_osrel()   { . /etc/os-release 2>/dev/null; printf '%s %s' "${NAME:-unknown}" "${VERSION_ID:-?}"; }
kt_is_root() { [ "$(id -u)" = 0 ]; }

kt_require_root() {
    kt_is_root || kt_die "must run as root (kernel tests load modules and touch /proc)"
}

kt_selinux_mode() { getenforce 2>/dev/null || echo "not-installed"; }

# Secure Boot decides whether a module that is not signed by a key this
# machine trusts can be loaded at all, so KUnit depends on the answer.
kt_secureboot() {
    local out=""
    if command -v mokutil >/dev/null 2>&1; then
        # On a legacy-BIOS machine mokutil says "EFI variables are not
        # supported on this system" on stderr and exits non-zero, so stderr
        # has to be kept or the answer comes back empty.
        out=$(mokutil --sb-state 2>&1 | head -1)
        # That message is mokutil telling us the machine has no EFI at all,
        # which is an answer about firmware, not about Secure Boot.
        case $out in *"not supported"*|*"EFI variables are not"*) out="" ;; esac
    fi
    if [ -z "$out" ] && [ -d /sys/firmware/efi ]; then
        local f
        f=$(ls /sys/firmware/efi/efivars/SecureBoot-* 2>/dev/null | head -1)
        if [ -n "$f" ]; then
            # The variable is a 4-byte attribute header plus one data byte.
            out=$(od -An -t u1 "$f" 2>/dev/null | \
                  awk '{print ($5==1) ? "SecureBoot enabled" : "SecureBoot disabled"}')
        fi
    fi
    if [ -z "$out" ]; then
        if [ -d /sys/firmware/efi ]; then
            out="SecureBoot unknown (EFI present, no SecureBoot variable)"
        else
            out="SecureBoot off (machine booted without EFI)"
        fi
    fi
    printf '%s' "$out"
}

# Non-zero when modules must carry a signature this kernel trusts.
kt_module_sig_enforced() {
    grep -q 'CONFIG_MODULE_SIG_FORCE=y' "/boot/config-$(uname -r)" 2>/dev/null && return 0
    [ "$(cat /sys/module/module/parameters/sig_enforce 2>/dev/null)" = Y ] && return 0
    return 1
}

# --- results ---------------------------------------------------------------
# One CSV row per test case.  Every engine writes the same five columns so the
# comparison in test-patch.sh does not need to know which engine produced them.
KT_CSV_HEADER='engine,collection,test,status,duration_s,message'

kt_csv_escape() {
    local s=$1
    s=${s//$'\n'/ }
    s=${s//$'\r'/ }
    s=${s//\"/\'\'}
    printf '"%s"' "$s"
}

# kt_record <csvfile> <engine> <collection> <test> <status> <duration> <message>
kt_record() {
    local f=$1; shift
    local row=""
    local v
    for v in "$@"; do row="$row$(kt_csv_escape "$v"),"; done
    printf '%s\n' "${row%,}" >> "$f"
}

kt_new_run_dir() {
    local base=$1 tag=${2:-run}
    local d="$base/$(date '+%Y%m%d-%H%M%S')-$tag"
    mkdir -p "$d" || kt_die "cannot create results dir $d"
    ln -sfn "$d" "$base/latest"
    printf '%s\n' "$d"
}

# --- dmesg ---------------------------------------------------------------
# Remember where the kernel log is now, so a test's own messages can be
# extracted afterwards without clearing the buffer (clearing loses evidence
# and would break anything else reading the log).
#
# The mark is a LINE COUNT, not a timestamp.  "dmesg --since" only resolves
# to the second, and KUnit gets through forty-odd modules in ten seconds, so
# a stack trace printed by one module lands inside the next module's window
# and gets blamed on it.  Counting lines is exact.
kt_dmesg_mark() {
    dmesg 2>/dev/null | wc -l
}

# kt_dmesg_since <mark>
kt_dmesg_since() {
    local mark=$1 now
    now=$(dmesg 2>/dev/null | wc -l)
    # If the ring buffer wrapped, the old mark points past the start of what
    # is left; show everything rather than nothing.
    if [ "$now" -lt "$mark" ]; then
        dmesg 2>/dev/null
    else
        dmesg 2>/dev/null | tail -n "+$(( mark + 1 ))"
    fi
}

# Words in the kernel log that mean the test hurt the machine.
KT_TAINT_PATTERN='BUG:|kernel BUG|Oops|general protection fault|WARNING: CPU|\
soft lockup|hard LOCKUP|rcu_sched detected|KASAN:|UBSAN:|list_add corruption|\
refcount_t:|Call Trace:'

kt_scan_dmesg() {
    local mark=$1 out=$2
    # Keep context: a bare "Call Trace:" on its own says nothing.  The lines
    # above name the warning and the process, and the lines below are the
    # stack itself.
    kt_dmesg_since "$mark" \
        | grep -E -B5 -A20 "$(printf '%s' "$KT_TAINT_PATTERN" | tr -d '\\\n')" \
        > "$out" 2>/dev/null
    [ -s "$out" ]
}

# Every KUnit test module, by module name, for the running kernel.
# Matching on the file name is not enough: ntb_msi_test looks like a KUnit
# test but depends on ntb, while time_test and lib_test really are KUnit.
# The thing they all have in common is a dependency on the kunit core.
# Walking every module takes a few seconds, so callers should do it once.
kt_list_kunit_modules() {
    local moddir=/lib/modules/$(uname -r)
    [ -d "$moddir" ] || return 0
    local f base
    find "$moddir" \( -name '*.ko' -o -name '*.ko.xz' -o -name '*.ko.gz' \
                    -o -name '*.ko.zst' \) 2>/dev/null | while IFS= read -r f; do
        base=${f##*/}
        base=${base%%.ko*}
        case ",$(modinfo -F depends "$f" 2>/dev/null)," in
            *,kunit,*) printf '%s\n' "$base" ;;
        esac
    done | sort -u
}

kt_taint() {
    local t; t=$(cat /proc/sys/kernel/tainted 2>/dev/null || echo 0)
    printf '%s' "$t"
}
