#!/bin/bash
#
# Kernel Tools Sanity Tests
#
# This script verifies that kernel tools:
# 1. Pass annocheck hardening tests
# 2. Function correctly at runtime
#
# Usage: ./kernel-tools-sanity-test.sh
#   This script tests the kernel-tools binaries and libraries.
#
# Created with assistance from Anthropic Claude

set -o pipefail

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

PASS_COUNT=0
FAIL_COUNT=0
SKIP_COUNT=0

log_pass() {
    echo -e "${GREEN}PASS${NC}: $1"
    ((PASS_COUNT++))
}

log_fail() {
    echo -e "${RED}FAIL${NC}: $1"
    ((FAIL_COUNT++))
}

log_skip() {
    echo -e "${YELLOW}SKIP${NC}: $1"
    ((SKIP_COUNT++))
}

log_info() {
    echo -e "INFO: $1"
}

log_section() {
    echo ""
    echo "========================================"
    echo "$1"
    echo "========================================"
}

# Check if a command exists
cmd_exists() {
    command -v "$1" &> /dev/null
}

# Run annocheck on a binary
check_annocheck() {
    local binary="$1"
    local name=$(basename "$binary")

    if [[ ! -f "$binary" ]]; then
        log_skip "annocheck $name - binary not found"
        return 0
    fi

    if ! cmd_exists annocheck; then
        log_skip "annocheck $name - annocheck not installed"
        return 0
    fi

    # annocheck only applies to ELF binaries (or RPMs). Many tools are scripts
    # (e.g. python/perl) and should be skipped rather than treated as failures.
    if cmd_exists file; then
        # Follow symlinks so SONAME links like libfoo.so.1 are detected as ELF.
        if ! file -L "$binary" 2>/dev/null | grep -q "ELF"; then
            log_skip "annocheck $name - not an ELF binary"
            return 0
        fi
    fi

    local output
    # annocheck treats symlinks as warnings unless -f is used
    if [[ -L "$binary" ]]; then
        output=$(annocheck -f "$binary" 2>&1)
    else
        output=$(annocheck "$binary" 2>&1)
    fi
    local rc=$?

    if echo "$output" | grep -qE ": PASS\.?$|Overall: PASS"; then
        log_pass "annocheck $name"
        return 0
    elif echo "$output" | grep -qE "MAYB:.*lto|MAYB: lto"; then
        log_fail "annocheck $name - still has MAYB:lto"
        echo "$output" | grep -E "MAYB|FAIL|Overall" | sed 's/^/    /' | head -5
        return 1
    else
        log_fail "annocheck $name"
        # Provide a useful snippet even if annocheck didn't emit Hardened: lines.
        local details
        details=$(echo "$output" | grep -E "MAYB|FAIL|Overall" | head -5)
        if [[ -n "$details" ]]; then
            echo "$details" | sed 's/^/    /'
        else
            echo "$output" | sed 's/^/    /' | head -5
        fi
        return 1
    fi
}

# Run a functional test
run_test() {
    local name="$1"
    local expect_rc="$2"
    shift 2

    local output rc
    output=$("$@" 2>&1)
    rc=$?

    if [[ $rc -eq $expect_rc ]]; then
        log_pass "$name"
        return 0
    else
        log_fail "$name (exit code $rc, expected $expect_rc)"
        echo "    Output: ${output:0:200}"
        return 1
    fi
}

# Run a command and treat any exit code as "pass" (useful for tools
# that may legitimately fail due to permissions/hardware access).
run_test_anyrc() {
    local name="$1"
    shift 1

    local output rc
    output=$("$@" 2>&1)
    rc=$?

    log_pass "$name (exit code $rc)"
    return 0
}

# Check if output contains expected string
run_test_grep() {
    local name="$1"
    local pattern="$2"
    shift 2

    local output rc
    output=$("$@" 2>&1)
    rc=$?

    if echo "$output" | grep -qE "$pattern"; then
        log_pass "$name"
        return 0
    else
        log_fail "$name (exit code $rc; pattern '$pattern' not found)"
        echo "    Output: ${output:0:200}"
        return 1
    fi
}

# Check output is non-empty
run_test_nonempty() {
    local name="$1"
    shift 1

    local output
    output=$("$@" 2>&1)

    if [[ -n "$output" ]]; then
        log_pass "$name"
        return 0
    else
        log_fail "$name (no output)"
        return 1
    fi
}

#############################################
# MAIN
#############################################

log_section "Kernel Tools Sanity Tests"
echo "Date: $(date)"
echo "Kernel: $(uname -r)"
echo ""

# Check if kernel-tools is installed
if ! rpm -q kernel-tools &>/dev/null; then
    echo -e "${RED}ERROR${NC}: kernel-tools package is not installed"
    echo "Please install kernel-tools before running this test:"
    echo "  dnf install kernel-tools"
    exit 1
fi

log_info "kernel-tools version: $(rpm -q kernel-tools)"

# Check if kernel-tools-debuginfo is installed (needed for annocheck)
if ! rpm -q kernel-tools-debuginfo &>/dev/null; then
    echo -e "${RED}ERROR${NC}: kernel-tools-debuginfo package is not installed"
    echo "Annocheck requires debuginfo to properly analyze binaries."
    echo "Without it, tests are silently skipped and results are unreliable."
    echo "Please install kernel-tools-debuginfo before running this test:"
    echo "  dnf install kernel-tools-debuginfo"
    exit 1
else
    log_info "kernel-tools-debuginfo version: $(rpm -q kernel-tools-debuginfo)"
fi
echo ""

# List of all tools that had MAYB:lto
# All arches:
TOOLS_ALL_ARCH=(
    bootconfig
    cpupower
    gpio-event-mon
    gpio-hammer
    gpio-watch
    lsgpio
    iio_event_monitor
    iio_generic_buffer
    lsiio
    page_owner_sort
    slabinfo
    tmon
)

# x86_64 only:
TOOLS_X86_ONLY=(
    centrino-decode
    intel-speed-select
    intel_sdsi
    powernow-k8-decode
    turbostat
    x86_energy_perf_policy
)

# Libraries
LIBS=(
    /usr/lib64/libcpupower.so.1
)

ARCH=$(uname -m)

#############################################
# Annocheck Tests
#############################################

log_section "Annocheck Verification"

for tool in "${TOOLS_ALL_ARCH[@]}"; do
    check_annocheck "/usr/bin/$tool"
done

if [[ "$ARCH" == "x86_64" ]]; then
    for tool in "${TOOLS_X86_ONLY[@]}"; do
        if [[ "$tool" == "intel_sdsi" ]]; then
            check_annocheck "/usr/sbin/$tool"
        else
            check_annocheck "/usr/bin/$tool"
        fi
    done
fi

for lib in "${LIBS[@]}"; do
    check_annocheck "$lib"
done

#############################################
# Functional Tests - cpupower
#############################################

log_section "Functional Tests - cpupower"

if cmd_exists cpupower; then
    run_test_grep "cpupower --version" "cpupower" cpupower --version
    run_test "cpupower frequency-info" 0 cpupower frequency-info
    run_test "cpupower idle-info" 0 cpupower idle-info
    run_test_nonempty "cpupower monitor (1 iteration)" timeout 2 cpupower monitor -c 0 -m Mperf sleep 0.5
else
    log_skip "cpupower tests - not installed"
fi

log_section "Functional Tests - bootconfig"

if cmd_exists bootconfig; then
    run_test_grep "bootconfig --help" "Usage|usage|bootconfig" bootconfig --help
else
    log_skip "bootconfig - not installed"
fi

#############################################
# Functional Tests - GPIO tools
#############################################

log_section "Functional Tests - GPIO tools"

if cmd_exists lsgpio; then
    # lsgpio may return empty if no GPIO chips, but shouldn't crash
    run_test "lsgpio runs" 0 lsgpio
else
    log_skip "lsgpio - not installed"
fi

if cmd_exists gpio-event-mon; then
    run_test_grep "gpio-event-mon --help" "Usage|usage|GPIO" gpio-event-mon --help
else
    log_skip "gpio-event-mon - not installed"
fi

if cmd_exists gpio-hammer; then
    run_test_grep "gpio-hammer --help" "Usage|usage|GPIO" gpio-hammer --help
else
    log_skip "gpio-hammer - not installed"
fi

if cmd_exists gpio-watch; then
    run_test_grep "gpio-watch --help" "Usage|usage|GPIO|gpiochip" gpio-watch --help
else
    log_skip "gpio-watch - not installed"
fi

#############################################
# Functional Tests - IIO tools
#############################################

log_section "Functional Tests - IIO tools"

if cmd_exists lsiio; then
    # lsiio returns 237 if no IIO devices available, which is expected
    output=$(lsiio 2>&1)
    rc=$?
    if [[ $rc -eq 0 ]] || [[ $rc -eq 237 && "$output" == *"No industrial I/O devices"* ]]; then
        log_pass "lsiio runs"
    else
        log_fail "lsiio runs (exit code $rc)"
        echo "    Output: ${output:0:200}"
    fi
else
    log_skip "lsiio - not installed"
fi

if cmd_exists iio_event_monitor; then
    run_test_grep "iio_event_monitor --help" "Usage|usage|device|IIO" iio_event_monitor --help
else
    log_skip "iio_event_monitor - not installed"
fi

if cmd_exists iio_generic_buffer; then
    run_test_grep "iio_generic_buffer --help" "Usage|usage|device|IIO" iio_generic_buffer --help
else
    log_skip "iio_generic_buffer - not installed"
fi

#############################################
# Functional Tests - Memory tools
#############################################

log_section "Functional Tests - Memory tools"

if cmd_exists slabinfo; then
    if [[ -r /proc/slabinfo ]]; then
        run_test_nonempty "slabinfo" slabinfo
        # Avoid slabinfo -s/--shrink (side-effecting). Use a non-destructive sort:
        # -S / --Size = sort output by object size
        run_test "slabinfo --Size (sort output by size)" 0 slabinfo --Size
    else
        log_skip "slabinfo - /proc/slabinfo not readable (need root)"
    fi
else
    log_skip "slabinfo - not installed"
fi

if cmd_exists page_owner_sort; then
    run_test_grep "page_owner_sort --help" "Usage|usage|page_owner" page_owner_sort
else
    log_skip "page_owner_sort - not installed"
fi

#############################################
# Functional Tests - Thermal (tmon)
#############################################

log_section "Functional Tests - Thermal (tmon)"

if cmd_exists tmon; then
    run_test_grep "tmon --help" "Usage|usage|tmon|thermal" tmon -h

    # Check if thermal zones exist
    if [[ -d /sys/devices/virtual/thermal ]]; then
        log_info "Thermal zones found"
        ls /sys/devices/virtual/thermal/ 2>/dev/null | head -5
    else
        log_info "No thermal zones available"
    fi
else
    log_skip "tmon - not installed"
fi

#############################################
# Functional Tests - x86_64 specific
#############################################

if [[ "$ARCH" == "x86_64" ]]; then
    log_section "Functional Tests - x86_64 specific"

    # turbostat
    if cmd_exists turbostat; then
        run_test_grep "turbostat --help" "Usage|usage|turbostat" turbostat --help

        # Quick run with 1 iteration - may fail on VMs or unsupported hardware
        if [[ -r /dev/cpu/0/msr ]] || [[ $EUID -eq 0 ]]; then
            output=$(timeout 3 turbostat --num_iterations 1 --quiet 2>&1)
            rc=$?
            if [[ $rc -eq 0 ]]; then
                log_pass "turbostat 1 iteration"
            elif echo "$output" | grep -qE "msr.*failed|not found|not supported|perf.*counter"; then
                # Tool ran but couldn't access hardware - that's OK, it didn't crash
                log_pass "turbostat runs (no hardware access)"
            else
                log_fail "turbostat 1 iteration (exit code $rc)"
                echo "    Output: ${output:0:200}"
            fi
        else
            log_skip "turbostat run - needs root or msr access"
        fi
    else
        log_skip "turbostat - not installed"
    fi

    # x86_energy_perf_policy
    if cmd_exists x86_energy_perf_policy; then
        run_test_grep "x86_energy_perf_policy --help" "Usage|usage|energy|EPB" x86_energy_perf_policy --help
    else
        log_skip "x86_energy_perf_policy - not installed"
    fi

    # intel-speed-select
    if cmd_exists intel-speed-select; then
        run_test_grep "intel-speed-select --help" "Usage|usage|Intel" intel-speed-select --help
    else
        log_skip "intel-speed-select - not installed"
    fi

    # intel_sdsi
    if cmd_exists intel_sdsi; then
        run_test_grep "intel_sdsi --help" "Usage|usage|SDSi|Intel" intel_sdsi --help
    else
        log_skip "intel_sdsi - not installed"
    fi

    # centrino-decode
    if cmd_exists centrino-decode; then
        # This reads MSRs, may need root
        run_test_anyrc "centrino-decode runs" centrino-decode
    else
        log_skip "centrino-decode - not installed"
    fi

    # powernow-k8-decode
    if cmd_exists powernow-k8-decode; then
        run_test_anyrc "powernow-k8-decode runs" powernow-k8-decode
    else
        log_skip "powernow-k8-decode - not installed"
    fi
fi

#############################################
# Library Tests
#############################################

log_section "Library Tests"

# Check libcpupower is loadable
if [[ -f /usr/lib64/libcpupower.so.1 ]]; then
    if cmd_exists cpupower; then
        run_test_grep "libcpupower linked" "libcpupower" ldd /usr/bin/cpupower
    fi
else
    log_skip "libcpupower.so - not installed"
fi


#############################################
# Summary
#############################################

log_section "Summary"

echo ""
echo "Results:"
echo -e "  ${GREEN}PASS${NC}: $PASS_COUNT"
echo -e "  ${RED}FAIL${NC}: $FAIL_COUNT"
echo -e "  ${YELLOW}SKIP${NC}: $SKIP_COUNT"
echo ""

if [[ $FAIL_COUNT -eq 0 ]]; then
    echo -e "${GREEN}All tests passed!${NC}"
    exit 0
else
    echo -e "${RED}Some tests failed.${NC}"
    exit 1
fi
