#!/bin/bash # # kpatch integration test framework # # Copyright (C) 2014 Josh Poimboeuf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA, # 02110-1301, USA. # # # This is a basic integration test framework for kpatch, which tests building, # loading, and unloading patches, as well as any other related custom tests. # # This script looks for test input files in the current directory. It expects # certain file naming conventions: # # - foo.patch: patch that should build successfully # # - foo-SLOW.patch: patch that should be skipped in the quick test # # - bar-FAIL.patch: patch that should fail to build # # - foo-LOADED.test: executable which tests whether the foo.patch module is # loaded. It will be used to test that loading/unloading the patch module # works as expected. # # Any other *.test files will be executed after all the patch modules have been # built from the *.patch files. They can be used for more custom tests above # and beyond the simple loading and unloading tests. shopt -s nullglob SCRIPTDIR="$(readlink -f $(dirname $(type -p $0)))" ROOTDIR="$(readlink -f $SCRIPTDIR/../..)" # TODO: option to use system-installed binaries instead KPATCH="sudo $ROOTDIR/kpatch/kpatch" RMMOD="sudo rmmod" unset CCACHE_HASHDIR KPATCHBUILD="$ROOTDIR"/kpatch-build/kpatch-build ERROR=0 LOG=test.log rm -f $LOG PATCHDIR="${PATCHDIR:-$PWD}" declare -a PATCH_LIST declare -a TEST_LIST usage() { echo "usage: $0 [options] [patch1 ... patchN]" >&2 echo " patchN Pathnames of patches to test" >&2 echo " -h, --help Show this help message" >&2 echo " -c, --cached Don't rebuild patch modules" >&2 echo " -d, --directory Patch directory" >&2 echo " -q, --quick Just combine all patches into one module for testing" >&2 } options=$(getopt -o hcd:q -l "help,cached,directory,quick" -- "$@") || exit 1 eval set -- "$options" while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; -c|--cached) SKIPBUILD=1 ;; -d|--directory) PATCHDIR="$2" shift ;; -q|--quick) QUICK=1 ;; *) [[ "$1" = "--" ]] && shift && continue PATCH_LIST+=("$1") ;; esac shift done if [[ ${#PATCH_LIST[@]} = 0 ]]; then PATCH_LIST=($PATCHDIR/*.patch) TEST_LIST=($PATCHDIR/*.test) if [[ ${#PATCH_LIST[@]} = 0 ]]; then echo "No patches found!" exit 1 fi else for file in "${PATCH_LIST[@]}"; do prefix=${file%%.patch} [[ -e "$prefix-FAIL.test" ]] && TEST_LIST+=("$prefix-FAIL.test") [[ -e "$prefix-LOADED.test" ]] && TEST_LIST+=("$prefix-LOADED.test") [[ -e "$prefix-SLOW.test" ]] && TEST_LIST+=("$prefix-SLOW.test") done fi error() { echo "ERROR: $@" |tee -a $LOG >&2 ERROR=$((ERROR + 1)) } log() { echo "$@" |tee -a $LOG } unload_all() { for i in `lsmod |egrep '^kpatch' |awk '{print $1}'`; do if [[ $i != kpatch ]]; then $KPATCH unload $i >> $LOG 2>&1 || error "\"kpatch unload $i\" failed" fi done if lsmod |egrep -q '^kpatch'; then $RMMOD kpatch >> $LOG 2>&1 || error "\"rmmod kpatch\" failed" fi } build_module() { file=$1 prefix=$(basename ${file%%.patch}) module=kpatch-$prefix.ko [[ $prefix = COMBINED ]] && return if [[ $prefix =~ -FAIL ]]; then shouldfail=1 else shouldfail=0 fi if [[ $SKIPBUILD -eq 1 ]]; then skip=0 [[ $shouldfail -eq 1 ]] && skip=1 [[ -e $module ]] && skip=1 [[ $skip -eq 1 ]] && log "skipping build: $prefix" && return fi log "build: $prefix" if ! $KPATCHBUILD $file >> $LOG 2>&1; then [[ $shouldfail -eq 0 ]] && error "$prefix: build failed" else [[ $shouldfail -eq 1 ]] && error "$prefix: build succeeded when it should have failed" fi } run_load_test() { file=$1 prefix=$(basename ${file%%.patch}) module=kpatch-$prefix.ko testprog="$(dirname $1)/$prefix-LOADED.test" [[ $prefix = COMBINED ]] && return [[ $prefix =~ -FAIL ]] && return if [[ ! -e $module ]]; then log "can't find $module, skipping" return fi if [[ -e $testprog ]]; then log "load test: $prefix" else log "load test: $prefix (no test prog)" fi if [[ -e $testprog ]] && $testprog >> $LOG 2>&1; then error "$prefix: $testprog succeeded before kpatch load" return fi if ! $KPATCH load $module >> $LOG 2>&1; then error "$prefix: kpatch load failed" return fi if [[ -e $testprog ]] && ! $testprog >> $LOG 2>&1; then error "$prefix: $testprog failed after kpatch load" fi if ! $KPATCH unload $module >> $LOG 2>&1; then error "$prefix: kpatch unload failed" return fi if [[ -e $testprog ]] && $testprog >> $LOG 2>&1; then error "$prefix: $testprog succeeded after kpatch unload" return fi } run_custom_test() { testprog=$1 prefix=$(basename ${file%%.test}}) [[ $testprog = *-LOADED.test ]] && return log "custom test: $prefix" if ! $testprog >> $LOG 2>&1; then error "$prefix: test failed" fi } build_combined_module() { if [[ $SKIPBUILD -eq 1 ]] && [[ -e kpatch-COMBINED.ko ]]; then log "skipping build: combined" return fi if ! which combinediff > /dev/null; then log "patchutils not installed, skipping combined module build" error "PLEASE INSTALL PATCHUTILS" return fi declare -a COMBINED_LIST for file in "${PATCH_LIST[@]}"; do [[ $file =~ -FAIL ]] && log "combine: skipping $file" && continue [[ $file =~ -SLOW ]] && log "combine: skipping $file" && continue COMBINED_LIST+=($file) done if [[ ${#COMBINED_LIST[@]} -le 1 ]]; then log "skipping build: combined (only ${#PATCH_LIST[@]} patch(es))" return fi rm -f COMBINED.patch TMP.patch first=1 for file in "${COMBINED_LIST[@]}"; do prefix=${file%%.patch} log "combine: $prefix" if [[ $first -eq 1 ]]; then cp -f $file COMBINED.patch first=0 continue fi combinediff COMBINED.patch $file > TMP.patch mv -f TMP.patch COMBINED.patch done log "build: combined module" if ! $KPATCHBUILD COMBINED.patch >> $LOG 2>&1; then error "combined build failed" fi } run_combined_test() { if [[ ! -e COMBINED.patch ]]; then return fi if [[ ! -e kpatch-COMBINED.ko ]]; then log "can't find kpatch-COMBINED.ko, skipping" return fi log "load test: combined module" unload_all for testprog in "${TEST_LIST[@]}"; do [[ $testprog != *-LOADED.test ]] && continue if $testprog >> $LOG 2>&1; then error "combined: $testprog succeeded before kpatch load" return fi done if ! $KPATCH load kpatch-COMBINED.ko >> $LOG 2>&1; then error "combined: kpatch load failed" return fi for testprog in "${TEST_LIST[@]}"; do [[ $testprog != *-LOADED.test ]] && continue if ! $testprog >> $LOG 2>&1; then error "combined: $testprog failed after kpatch load" fi done if ! $KPATCH unload kpatch-COMBINED.ko >> $LOG 2>&1; then error "combined: kpatch unload failed" return fi for testprog in "${TEST_LIST[@]}"; do [[ $testprog != *-LOADED.test ]] && continue if $testprog >> $LOG 2>&1; then error "combined: $testprog succeeded after kpatch unload" return fi done } echo "clearing printk buffer" sudo dmesg -C if [[ $QUICK != 1 ]]; then for file in "${PATCH_LIST[@]}"; do build_module $file done fi build_combined_module unload_all if [[ $QUICK != 1 ]]; then for file in "${PATCH_LIST[@]}"; do run_load_test $file done fi run_combined_test if [[ $QUICK != 1 ]]; then for testprog in "${TEST_LIST[@]}"; do unload_all run_custom_test $testprog done fi unload_all dmesg |grep -q "Call Trace" && error "kernel error detected in printk buffer" if [[ $ERROR -gt 0 ]]; then log "$ERROR errors encountered" echo "see test.log for more information" else log "SUCCESS" fi exit $ERROR