#!/bin/bash # SPDX-License-Identifier: GPL-2.0 # Copyright (C) 2026, Google LLC. # # Generic Boot Tracing Test Harness # Builds a lightweight initramfs, applies bootconfigs and kernel cmdline parameters, # boots QEMU, and verifies tracefs configuration using test scripts. set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -d "$SCRIPT_DIR/boottime" ]; then SCRIPT_DIR="$SCRIPT_DIR/boottime" fi KERNEL_SRC="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" # Source KTAP helpers if [ -f "$SCRIPT_DIR/../../kselftest/ktap_helpers.sh" ]; then KSELFTEST_DIR="$(cd "$SCRIPT_DIR/../../kselftest" && pwd)" elif [ -f "$SCRIPT_DIR/../kselftest/ktap_helpers.sh" ]; then KSELFTEST_DIR="$(cd "$SCRIPT_DIR/../kselftest" && pwd)" else echo "Error: ktap_helpers.sh not found relative to $SCRIPT_DIR" >&2 exit 1 fi . "$KSELFTEST_DIR/ktap_helpers.sh" BOOTCONFIG="${BOOTCONFIG:-"$KERNEL_SRC/tools/bootconfig/bootconfig"}" KERNEL="${KERNEL:-"$KERNEL_SRC/arch/x86/boot/bzImage"}" QEMU="${QEMU:-"qemu-system-x86_64"}" BUSYBOX="${BUSYBOX:-"$(which busybox 2>/dev/null || echo "/usr/bin/busybox")"}" TIMEOUT="${TIMEOUT:-60s}" LOGDIR="${LOGDIR:-""}" TARGET_TEST="all" BUSYBOX_APPLETS=( sh cat mount umount echo sync reboot poweroff cpio grep sleep sed ) usage() { echo "Usage: $0 [options]" echo "Options:" echo " -b, --bootconfig PATH Path to bootconfig tool (default: $BOOTCONFIG)" echo " -k, --kernel PATH Path to kernel image (default: $KERNEL)" echo " -q, --qemu PATH Path to QEMU executable (default: $QEMU)" echo " -B, --busybox PATH Path to busybox binary (default: $BUSYBOX)" echo " -T, --timeout DURATION QEMU execution timeout (default: $TIMEOUT)" echo " -l, --logdir DIR Directory to save QEMU log files" echo " -t, --test NAME Run specific test (e.g. 01-kprobe) or 'all'" echo " -h, --help Show this help message" exit 1 } while [ $# -gt 0 ]; do case "$1" in -b|--bootconfig) BOOTCONFIG="$2" shift 2 ;; -k|--kernel) KERNEL="$2" shift 2 ;; -q|--qemu) QEMU="$2" shift 2 ;; -B|--busybox) BUSYBOX="$2" shift 2 ;; -T|--timeout) TIMEOUT="$2" shift 2 ;; -l|--logdir) LOGDIR="$2" shift 2 ;; -t|--test) TARGET_TEST="$2" shift 2 ;; -h|--help) usage ;; *) echo "Unknown option: $1" usage ;; esac done # Check host architecture HOST_ARCH="$(uname -m)" case "$HOST_ARCH" in x86_64|i686|i386|x86) ;; *) if [ "$KERNEL" = "$KERNEL_SRC/arch/x86/boot/bzImage" ]; then ktap_print_header ktap_skip_all \ "Default boot tracing test is x86 only ($HOST_ARCH)" exit 0 fi ;; esac # Ensure bootconfig tool is built if Makefile exists if [ ! -x "$BOOTCONFIG" ]; then if [ -f "$KERNEL_SRC/tools/bootconfig/Makefile" ]; then make -C "$KERNEL_SRC/tools/bootconfig" > /dev/null 2>&1 || true fi if [ ! -x "$BOOTCONFIG" ]; then BOOTCONFIG="$(which bootconfig 2>/dev/null || true)" fi fi if [ ! -x "$BOOTCONFIG" ]; then ktap_print_header ktap_skip_all "bootconfig tool not found at $BOOTCONFIG" exit 0 fi if [ ! -f "$KERNEL" ]; then ktap_print_header ktap_skip_all "Kernel image not found at $KERNEL" exit 0 fi if ! command -v "$QEMU" >/dev/null 2>&1; then ktap_print_header ktap_skip_all "QEMU binary not found: $QEMU" exit 0 fi if [ ! -x "$BUSYBOX" ]; then ktap_print_header ktap_skip_all "busybox binary not found at $BUSYBOX" exit 0 fi # Verify file command existence and architecture compatibility if ! command -v file >/dev/null 2>&1; then ktap_print_header ktap_skip_all "file tool not found" exit 0 fi KERNEL_INFO="$(file -bL "$KERNEL" 2>/dev/null || true)" case "$KERNEL_INFO" in *x86*) ;; *) ktap_print_header ktap_skip_all "Kernel architecture is not x86 ($KERNEL_INFO)" exit 0 ;; esac BUSYBOX_INFO="$(file -bL "$BUSYBOX" 2>/dev/null || true)" case "$BUSYBOX_INFO" in *x86-64*|*x86_64*|*80386*|*i386*) ;; *) ktap_print_header ktap_skip_all \ "busybox binary architecture is incompatible with x86 kernel ($BUSYBOX_INFO)" exit 0 ;; esac if ! command -v cpio >/dev/null 2>&1; then ktap_print_header ktap_skip_all "cpio tool not found" exit 0 fi if ! command -v timeout >/dev/null 2>&1; then ktap_print_header ktap_skip_all "timeout tool not found" exit 0 fi double_timeout() { local val="$1" if [[ "$val" =~ ^([0-9]+)([a-z]*)$ ]]; then local num="${BASH_REMATCH[1]}" local unit="${BASH_REMATCH[2]}" echo "$((num * 2))$unit" else echo "120s" fi } run_single_test() { local test_script="$1" local name local dir name="$(basename "$test_script" .sh)" dir="$(dirname "$test_script")" local bconf_file="" if [ -f "$SCRIPT_DIR/bootconfigs/$name.bconf" ]; then bconf_file="$SCRIPT_DIR/bootconfigs/$name.bconf" elif [ -f "$dir/$name.bconf" ]; then bconf_file="$dir/$name.bconf" fi local test_cmdline="" if [ -f "$SCRIPT_DIR/cmdlines/$name.cmdline" ]; then test_cmdline="$(cat "$SCRIPT_DIR/cmdlines/$name.cmdline")" elif [ -f "$dir/$name.cmdline" ]; then test_cmdline="$(cat "$dir/$name.cmdline")" fi local inline_cmdline inline_cmdline="$(grep -E '^# *CMDLINE:' "$test_script" | sed 's/^# *CMDLINE://' | xargs || true)" if [ -n "$inline_cmdline" ]; then test_cmdline="$test_cmdline $inline_cmdline" fi local test_qemuopts="" if [ -f "$SCRIPT_DIR/qemuopts/$name.qemuopts" ]; then test_qemuopts="$(cat "$SCRIPT_DIR/qemuopts/$name.qemuopts")" elif [ -f "$dir/$name.qemuopts" ]; then test_qemuopts="$(cat "$dir/$name.qemuopts")" fi local inline_qemuopts inline_qemuopts="$(grep -E '^# *QEMUOPTS:' "$test_script" | sed 's/^# *QEMUOPTS://' | xargs || true)" if [ -n "$inline_qemuopts" ]; then test_qemuopts="$test_qemuopts $inline_qemuopts" fi local test_timeout="$TIMEOUT" local inline_timeout inline_timeout="$(grep -E '^# *TIMEOUT:' "$test_script" | sed 's/^# *TIMEOUT://' | xargs || true)" if [ -n "$inline_timeout" ]; then test_timeout="$inline_timeout" fi local is_reboot=0 if grep -q -E '^# *REBOOT: *1' "$test_script" || [ -f "$dir/$name.reboot" ]; then is_reboot=1 fi local effective_timeout="$test_timeout" if [ "$is_reboot" -eq 1 ]; then effective_timeout="$(double_timeout "$test_timeout")" fi local workdir workdir="$(mktemp -d)" if [ -z "$workdir" ] || [ ! -d "$workdir" ]; then ktap_test_fail "$name (failed to create temporary directory)" return 0 fi trap 'rm -rf "$workdir"' EXIT local test_applets=("${BUSYBOX_APPLETS[@]}") local inline_applets inline_applets="$(grep -E '^# *APPLETS:' "$test_script" | sed 's/^# *APPLETS://' | xargs || true)" if [ -n "$inline_applets" ]; then read -r -a extra_applets <<< "$inline_applets" || true test_applets+=("${extra_applets[@]}") fi local rootfs="$workdir/rootfs" mkdir -p "$rootfs"/{bin,sbin,etc,proc,sys,dev,tmp} cp -L "$BUSYBOX" "$rootfs/bin/busybox" chmod +x "$rootfs/bin/busybox" (cd "$rootfs/bin" && for applet in "${test_applets[@]}"; do ln -sf busybox "$applet"; done) if command -v ldd >/dev/null 2>&1; then for lib in $(ldd "$BUSYBOX" 2>/dev/null | grep -o '/[^ ]*' || true); do if [ -f "$lib" ]; then mkdir -p "$rootfs$(dirname "$lib")" cp -L "$lib" "$rootfs$lib" 2>/dev/null || true fi done fi cat << 'EOF' > "$rootfs/init" #!/bin/sh mount -t proc proc /proc 2>/dev/null mount -t sysfs sys /sys 2>/dev/null mount -t tracefs nodev /sys/kernel/tracing 2>/dev/null /bin/check_test.sh RET=$? if [ $RET -eq 0 ]; then echo "TEST RESULT: PASS" else echo "TEST RESULT: FAIL" fi sync echo o > /proc/sysrq-trigger 2>/dev/null || true poweroff -f 2>/dev/null || reboot -f 2>/dev/null || true while true; do sleep 100 2>/dev/null || break; done EOF chmod +x "$rootfs/init" cp "$test_script" "$rootfs/bin/check_test.sh" chmod +x "$rootfs/bin/check_test.sh" local initramfs="$workdir/initramfs.cpio" (cd "$rootfs" && find . | cpio -o -H newc --quiet) > "$initramfs" if [ -n "$bconf_file" ] && [ -f "$bconf_file" ]; then "$BOOTCONFIG" -a "$bconf_file" "$initramfs" > /dev/null fi local logfile local base_cmdline="bootconfig console=tty0 console=ttyS0 panic=-1" if [ -n "$test_cmdline" ]; then base_cmdline="$base_cmdline $test_cmdline" fi if [ -n "$LOGDIR" ]; then mkdir -p "$LOGDIR" logfile="$LOGDIR/$name.log" base_cmdline="$base_cmdline dump_bconf" else logfile="$workdir/qemu.log" base_cmdline="quiet $base_cmdline" fi local qemu_args=() if [ -n "$test_qemuopts" ]; then read -r -d '' -a qemu_args <<< "$test_qemuopts" || true fi if [ "$is_reboot" -ne 1 ]; then qemu_args+=("-no-reboot") fi timeout "$effective_timeout" "$QEMU" -kernel "$KERNEL" \ -initrd "$initramfs" \ -append "$base_cmdline" \ -display none \ -serial stdio \ "${qemu_args[@]}" < /dev/null > "$logfile" 2>&1 || true if grep -q "TEST RESULT: PASS" "$logfile"; then ktap_test_pass "$name" else ktap_test_fail "$name" ktap_print_msg "QEMU Console Output for $name:" while IFS= read -r line; do ktap_print_msg "$line" done < "$logfile" fi rm -rf "$workdir" trap - EXIT return 0 } TEST_SCRIPTS=() if [ -d "$SCRIPT_DIR/tests" ]; then while IFS= read -r f; do [ -n "$f" ] && TEST_SCRIPTS+=("$f") done < <(find "$SCRIPT_DIR/tests" -type f -name "*.sh" | sort) fi TOTAL_TESTS=0 for test_script in "${TEST_SCRIPTS[@]}"; do name="$(basename "$test_script" .sh)" if [ "$TARGET_TEST" != "all" ] && [ "$TARGET_TEST" != "$name" ]; then continue fi TOTAL_TESTS=$((TOTAL_TESTS + 1)) done ktap_print_header ktap_set_plan "$TOTAL_TESTS" for test_script in "${TEST_SCRIPTS[@]}"; do name="$(basename "$test_script" .sh)" if [ "$TARGET_TEST" != "all" ] && [ "$TARGET_TEST" != "$name" ]; then continue fi run_single_test "$test_script" done ktap_finished