1#!/bin/bash 2# SPDX-License-Identifier: GPL-2.0 3# 4# Copyright (c) 2025 Red Hat 5# Copyright (c) 2025 Meta Platforms, Inc. and affiliates 6# 7# Dependencies: 8# * virtme-ng 9# * busybox-static (used by virtme-ng) 10# * qemu (used by virtme-ng) 11 12readonly SCRIPT_DIR="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" 13readonly KERNEL_CHECKOUT=$(realpath "${SCRIPT_DIR}"/../../../../) 14 15source "${SCRIPT_DIR}"/../kselftest/ktap_helpers.sh 16 17readonly HID_BPF_TEST="${SCRIPT_DIR}"/hid_bpf 18readonly HIDRAW_TEST="${SCRIPT_DIR}"/hidraw 19readonly HID_BPF_PROGS="${KERNEL_CHECKOUT}/drivers/hid/bpf/progs" 20readonly SSH_GUEST_PORT=22 21readonly WAIT_PERIOD=3 22readonly WAIT_PERIOD_MAX=60 23readonly WAIT_TOTAL=$(( WAIT_PERIOD * WAIT_PERIOD_MAX )) 24readonly QEMU_PIDFILE=$(mktemp /tmp/qemu_hid_vmtest_XXXX.pid) 25 26readonly QEMU_OPTS="\ 27 --pidfile ${QEMU_PIDFILE} \ 28" 29readonly KERNEL_CMDLINE="" 30readonly LOG=$(mktemp /tmp/hid_vmtest_XXXX.log) 31readonly TEST_NAMES=(vm_hid_bpf vm_hidraw vm_pytest) 32readonly TEST_DESCS=( 33 "Run hid_bpf tests in the VM." 34 "Run hidraw tests in the VM." 35 "Run the hid-tools test-suite in the VM." 36) 37 38VERBOSE=0 39SHELL_MODE=0 40BUILD_HOST="" 41BUILD_HOST_PODMAN_CONTAINER_NAME="" 42 43usage() { 44 local name 45 local desc 46 local i 47 48 echo 49 echo "$0 [OPTIONS] [TEST]... [-- tests-args]" 50 echo "If no TEST argument is given, all tests will be run." 51 echo 52 echo "Options" 53 echo " -b: build the kernel from the current source tree and use it for guest VMs" 54 echo " -H: hostname for remote build host (used with -b)" 55 echo " -p: podman container name for remote build host (used with -b)" 56 echo " Example: -H beefyserver -p vng" 57 echo " -q: set the path to or name of qemu binary" 58 echo " -s: start a shell in the VM instead of running tests" 59 echo " -v: more verbose output (can be repeated multiple times)" 60 echo 61 echo "Available tests" 62 63 for ((i = 0; i < ${#TEST_NAMES[@]}; i++)); do 64 name=${TEST_NAMES[${i}]} 65 desc=${TEST_DESCS[${i}]} 66 printf "\t%-35s%-35s\n" "${name}" "${desc}" 67 done 68 echo 69 70 exit 1 71} 72 73die() { 74 echo "$*" >&2 75 exit "${KSFT_FAIL}" 76} 77 78vm_ssh() { 79 # vng --ssh-client keeps shouting "Warning: Permanently added 'virtme-ng%22' 80 # (ED25519) to the list of known hosts.", 81 # So replace the command with what's actually called and add the "-q" option 82 stdbuf -oL ssh -q \ 83 -F ${HOME}/.cache/virtme-ng/.ssh/virtme-ng-ssh.conf \ 84 -l root virtme-ng%${SSH_GUEST_PORT} \ 85 "$@" 86 return $? 87} 88 89cleanup() { 90 if [[ -s "${QEMU_PIDFILE}" ]]; then 91 pkill -SIGTERM -F "${QEMU_PIDFILE}" > /dev/null 2>&1 92 fi 93 94 # If failure occurred during or before qemu start up, then we need 95 # to clean this up ourselves. 96 if [[ -e "${QEMU_PIDFILE}" ]]; then 97 rm "${QEMU_PIDFILE}" 98 fi 99} 100 101check_args() { 102 local found 103 104 for arg in "$@"; do 105 found=0 106 for name in "${TEST_NAMES[@]}"; do 107 if [[ "${name}" = "${arg}" ]]; then 108 found=1 109 break 110 fi 111 done 112 113 if [[ "${found}" -eq 0 ]]; then 114 echo "${arg} is not an available test" >&2 115 usage 116 fi 117 done 118 119 for arg in "$@"; do 120 if ! command -v > /dev/null "test_${arg}"; then 121 echo "Test ${arg} not found" >&2 122 usage 123 fi 124 done 125} 126 127check_deps() { 128 for dep in vng ${QEMU} busybox pkill ssh pytest; do 129 if [[ ! -x $(command -v "${dep}") ]]; then 130 echo -e "skip: dependency ${dep} not found!\n" 131 exit "${KSFT_SKIP}" 132 fi 133 done 134 135 if [[ ! -x $(command -v "${HID_BPF_TEST}") ]]; then 136 printf "skip: %s not found!" "${HID_BPF_TEST}" 137 printf " Please build the kselftest hid_bpf target.\n" 138 exit "${KSFT_SKIP}" 139 fi 140 141 if [[ ! -x $(command -v "${HIDRAW_TEST}") ]]; then 142 printf "skip: %s not found!" "${HIDRAW_TEST}" 143 printf " Please build the kselftest hidraw target.\n" 144 exit "${KSFT_SKIP}" 145 fi 146} 147 148check_vng() { 149 local tested_versions 150 local version 151 local ok 152 153 tested_versions=("1.36" "1.37") 154 version="$(vng --version)" 155 156 ok=0 157 for tv in "${tested_versions[@]}"; do 158 if [[ "${version}" == *"${tv}"* ]]; then 159 ok=1 160 break 161 fi 162 done 163 164 if [[ ! "${ok}" -eq 1 ]]; then 165 printf "warning: vng version '%s' has not been tested and may " "${version}" >&2 166 printf "not function properly.\n\tThe following versions have been tested: " >&2 167 echo "${tested_versions[@]}" >&2 168 fi 169} 170 171handle_build() { 172 if [[ ! "${BUILD}" -eq 1 ]]; then 173 return 174 fi 175 176 if [[ ! -d "${KERNEL_CHECKOUT}" ]]; then 177 echo "-b requires vmtest.sh called from the kernel source tree" >&2 178 exit 1 179 fi 180 181 pushd "${KERNEL_CHECKOUT}" &>/dev/null 182 183 if ! vng --kconfig --config "${SCRIPT_DIR}"/config; then 184 die "failed to generate .config for kernel source tree (${KERNEL_CHECKOUT})" 185 fi 186 187 local vng_args=("-v" "--config" "${SCRIPT_DIR}/config" "--build") 188 189 if [[ -n "${BUILD_HOST}" ]]; then 190 vng_args+=("--build-host" "${BUILD_HOST}") 191 fi 192 193 if [[ -n "${BUILD_HOST_PODMAN_CONTAINER_NAME}" ]]; then 194 vng_args+=("--build-host-exec-prefix" \ 195 "podman exec -ti ${BUILD_HOST_PODMAN_CONTAINER_NAME}") 196 fi 197 198 if ! vng "${vng_args[@]}"; then 199 die "failed to build kernel from source tree (${KERNEL_CHECKOUT})" 200 fi 201 202 if ! make -j$(nproc) -C "${HID_BPF_PROGS}"; then 203 die "failed to build HID bpf objects from source tree (${HID_BPF_PROGS})" 204 fi 205 206 if ! make -j$(nproc) -C "${SCRIPT_DIR}"; then 207 die "failed to build HID selftests from source tree (${SCRIPT_DIR})" 208 fi 209 210 popd &>/dev/null 211} 212 213vm_start() { 214 local logfile=/dev/null 215 local verbose_opt="" 216 local kernel_opt="" 217 local qemu 218 219 qemu=$(command -v "${QEMU}") 220 221 if [[ "${VERBOSE}" -eq 2 ]]; then 222 verbose_opt="--verbose" 223 logfile=/dev/stdout 224 fi 225 226 # If we are running from within the kernel source tree, use the kernel source tree 227 # as the kernel to boot, otherwise use the currently running kernel. 228 if [[ "$(realpath "$(pwd)")" == "${KERNEL_CHECKOUT}"* ]]; then 229 kernel_opt="${KERNEL_CHECKOUT}" 230 fi 231 232 vng \ 233 --run \ 234 ${kernel_opt} \ 235 ${verbose_opt} \ 236 --qemu-opts="${QEMU_OPTS}" \ 237 --qemu="${qemu}" \ 238 --user root \ 239 --append "${KERNEL_CMDLINE}" \ 240 --ssh "${SSH_GUEST_PORT}" \ 241 --rw &> ${logfile} & 242 243 local vng_pid=$! 244 local elapsed=0 245 246 while [[ ! -s "${QEMU_PIDFILE}" ]]; do 247 if ! kill -0 "${vng_pid}" 2>/dev/null; then 248 echo "vng process (PID ${vng_pid}) exited early, check logs for details" >&2 249 die "failed to boot VM" 250 fi 251 252 if [[ ${elapsed} -ge ${WAIT_TOTAL} ]]; then 253 echo "Timed out after ${WAIT_TOTAL} seconds waiting for VM to boot" >&2 254 die "failed to boot VM" 255 fi 256 257 sleep 1 258 elapsed=$((elapsed + 1)) 259 done 260} 261 262vm_wait_for_ssh() { 263 local i 264 265 i=0 266 while true; do 267 if [[ ${i} -gt ${WAIT_PERIOD_MAX} ]]; then 268 die "Timed out waiting for guest ssh" 269 fi 270 if vm_ssh -- true; then 271 break 272 fi 273 i=$(( i + 1 )) 274 sleep ${WAIT_PERIOD} 275 done 276} 277 278vm_mount_bpffs() { 279 vm_ssh -- mount bpffs -t bpf /sys/fs/bpf 280} 281 282__log_stdin() { 283 stdbuf -oL awk '{ printf "%s:\t%s\n","'"${prefix}"'", $0; fflush() }' 284} 285 286__log_args() { 287 echo "$*" | awk '{ printf "%s:\t%s\n","'"${prefix}"'", $0 }' 288} 289 290log() { 291 local verbose="$1" 292 shift 293 294 local prefix="$1" 295 296 shift 297 local redirect= 298 if [[ ${verbose} -le 0 ]]; then 299 redirect=/dev/null 300 else 301 redirect=/dev/stdout 302 fi 303 304 if [[ "$#" -eq 0 ]]; then 305 __log_stdin | tee -a "${LOG}" > ${redirect} 306 else 307 __log_args "$@" | tee -a "${LOG}" > ${redirect} 308 fi 309} 310 311log_setup() { 312 log $((VERBOSE-1)) "setup" "$@" 313} 314 315log_host() { 316 local testname=$1 317 318 shift 319 log $((VERBOSE-1)) "test:${testname}:host" "$@" 320} 321 322log_guest() { 323 local testname=$1 324 325 shift 326 log ${VERBOSE} "# test:${testname}" "$@" 327} 328 329test_vm_hid_bpf() { 330 local testname="${FUNCNAME[0]#test_}" 331 332 vm_ssh -- "${HID_BPF_TEST}" \ 333 2>&1 | log_guest "${testname}" 334 335 return ${PIPESTATUS[0]} 336} 337 338test_vm_hidraw() { 339 local testname="${FUNCNAME[0]#test_}" 340 341 vm_ssh -- "${HIDRAW_TEST}" \ 342 2>&1 | log_guest "${testname}" 343 344 return ${PIPESTATUS[0]} 345} 346 347test_vm_pytest() { 348 local testname="${FUNCNAME[0]#test_}" 349 350 shift 351 352 vm_ssh -- pytest ${SCRIPT_DIR}/tests --color=yes "$@" \ 353 2>&1 | log_guest "${testname}" 354 355 return ${PIPESTATUS[0]} 356} 357 358run_test() { 359 local vm_oops_cnt_before 360 local vm_warn_cnt_before 361 local vm_oops_cnt_after 362 local vm_warn_cnt_after 363 local name 364 local rc 365 366 vm_oops_cnt_before=$(vm_ssh -- dmesg | grep -c -i 'Oops') 367 vm_error_cnt_before=$(vm_ssh -- dmesg --level=err | wc -l) 368 369 name=$(echo "${1}" | awk '{ print $1 }') 370 eval test_"${name}" "$@" 371 rc=$? 372 373 vm_oops_cnt_after=$(vm_ssh -- dmesg | grep -i 'Oops' | wc -l) 374 if [[ ${vm_oops_cnt_after} -gt ${vm_oops_cnt_before} ]]; then 375 echo "FAIL: kernel oops detected on vm" | log_host "${name}" 376 rc=$KSFT_FAIL 377 fi 378 379 vm_error_cnt_after=$(vm_ssh -- dmesg --level=err | wc -l) 380 if [[ ${vm_error_cnt_after} -gt ${vm_error_cnt_before} ]]; then 381 echo "FAIL: kernel error detected on vm" | log_host "${name}" 382 vm_ssh -- dmesg --level=err | log_host "${name}" 383 rc=$KSFT_FAIL 384 fi 385 386 return "${rc}" 387} 388 389QEMU="qemu-system-$(uname -m)" 390 391while getopts :hvsbq:H:p: o 392do 393 case $o in 394 v) VERBOSE=$((VERBOSE+1));; 395 s) SHELL_MODE=1;; 396 b) BUILD=1;; 397 q) QEMU=$OPTARG;; 398 H) BUILD_HOST=$OPTARG;; 399 p) BUILD_HOST_PODMAN_CONTAINER_NAME=$OPTARG;; 400 h|*) usage;; 401 esac 402done 403shift $((OPTIND-1)) 404 405trap cleanup EXIT 406 407PARAMS="" 408 409if [[ ${#} -eq 0 ]]; then 410 ARGS=("${TEST_NAMES[@]}") 411else 412 ARGS=() 413 COUNT=0 414 for arg in $@; do 415 COUNT=$((COUNT+1)) 416 if [[ x"$arg" == x"--" ]]; then 417 break 418 fi 419 ARGS+=($arg) 420 done 421 shift $COUNT 422 PARAMS="$@" 423fi 424 425if [[ "${SHELL_MODE}" -eq 0 ]]; then 426 check_args "${ARGS[@]}" 427 echo "1..${#ARGS[@]}" 428fi 429check_deps 430check_vng 431handle_build 432 433log_setup "Booting up VM" 434vm_start 435vm_wait_for_ssh 436vm_mount_bpffs 437log_setup "VM booted up" 438 439if [[ "${SHELL_MODE}" -eq 1 ]]; then 440 log_setup "Starting interactive shell in VM" 441 echo "Starting shell in VM. Use 'exit' to quit and shutdown the VM." 442 CURRENT_DIR="$(pwd)" 443 vm_ssh -t -- "cd '${CURRENT_DIR}' && exec bash -l" 444 exit "$KSFT_PASS" 445fi 446 447cnt_pass=0 448cnt_fail=0 449cnt_skip=0 450cnt_total=0 451for arg in "${ARGS[@]}"; do 452 run_test "${arg}" "${PARAMS}" 453 rc=$? 454 if [[ ${rc} -eq $KSFT_PASS ]]; then 455 cnt_pass=$(( cnt_pass + 1 )) 456 echo "ok ${cnt_total} ${arg}" 457 elif [[ ${rc} -eq $KSFT_SKIP ]]; then 458 cnt_skip=$(( cnt_skip + 1 )) 459 echo "ok ${cnt_total} ${arg} # SKIP" 460 elif [[ ${rc} -eq $KSFT_FAIL ]]; then 461 cnt_fail=$(( cnt_fail + 1 )) 462 echo "not ok ${cnt_total} ${arg} # exit=$rc" 463 fi 464 cnt_total=$(( cnt_total + 1 )) 465done 466 467echo "SUMMARY: PASS=${cnt_pass} SKIP=${cnt_skip} FAIL=${cnt_fail}" 468echo "Log: ${LOG}" 469 470if [ $((cnt_pass + cnt_skip)) -eq ${cnt_total} ]; then 471 exit "$KSFT_PASS" 472else 473 exit "$KSFT_FAIL" 474fi 475