Skip to content

Commit

Permalink
testsuite: add generic test script for incremental depmod
Browse files Browse the repository at this point in the history
Add a test script to test incremental depmod on a real kernel, as an addition
to the normal kmod CI. This script is meant to be run against the installed
kernel on a test system.

This test script compares the results (i.e. the modules.* files generated by
depmod) of incremental depmod with a full depmod run. The expectation is that
the results are always identical.

The script takes a list of module basenames as arguments, the "test modules",
like this:

   test-incremental.sh scsi_mod drm_display_helper

1. The script looks for a kernel module tree in the system it's running on
and copies this tree to the working directory, and runs a full depmod there.
2. It copies the entire tree again, deletes the test modules and all modules
depending on them, and runs full depmod once more.
3. It copies the tree created in 2., re-adds the previously deleted modules,
and runs "depmod --incremental" with the added modules as arguments, and
compares the results with those from step 1.
4. It copies the tree created in 2., re-adds the previously deleted modules,
runs "depmod --incremental --all", and compares the results with those from
step 1.
5. It copies the tree created in 2, runs "depmod --incremental --all", and
compares the results with those from step 2.

If any differences are found in step 3., 4., or 5., the script will report
them.

By default, all operations are done below a temporary directory under /tmp,
so no changes are made to the system. The autodetected parameters should work
on most systems. Otherwise, the script has various options to override them.

Copying this script into meson's build directory requires meson's
"fs.copyfile" feature, which was introduced in meson 0.64. The meson build
must therefore be disabled on Ubuntu 22.04, which still ships meson 0.61.

Signed-off-by: Martin Wilck <[email protected]>
  • Loading branch information
mwilck committed Nov 18, 2024
1 parent 7902117 commit 6bf3c3d
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ jobs:
multilib: 'true'
- name: 'ubuntu:24.04'
multilib: 'true'
exclude:
- build: 'meson'
container:
name: 'ubuntu:22.04'
multilib: 'true'

container:
image: ${{ matrix.container.name }}
Expand Down
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ project(
'c',
version : '33',
license : ['LGPLv2.1', 'GPL-2.0-or-later'],
meson_version : '>=0.61.0',
meson_version : '>=0.64.0',
default_options : [
'c_std=gnu11',
'b_pie=true',
Expand Down
4 changes: 4 additions & 0 deletions testsuite/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ if not get_option('tools')
error('The test suite requires also building the tools')
endif

fs = import('fs')

build_module_playground = custom_target(
'build-module-playground',
command : [
Expand Down Expand Up @@ -122,3 +124,5 @@ foreach input : _testsuite
depends : [exec, internal_kmod_symlinks, create_rootfs, test_override_mods],
)
endforeach

test_incremental = fs.copyfile('test-incremental.sh', 'test-incremental.sh')
305 changes: 305 additions & 0 deletions testsuite/test-incremental.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
#! /bin/bash
export TIMEFORMAT="elapsed time: %2Rs (%2U user / %2S system)"
export LC_ALL=C

set -e -E
trap 'echo $ME: error in $BASH_COMMAND >&2; exit 1' ERR

CLEANUP=:
trap 'trap - ERR; eval "$CLEANUP"' 0

ME=${0##*/}
BASEDIR=
OUTPUTDIR=
KVER=
SYMVERS=
FILESYMS=
DEPMOD=
KEEP=
ERR=0
: "${DEPMOD_OPTS:=}"

usage() {
cat <<EOF
Usage: $ME [OPTION]... [MODULE]...
MODULE is a module basename such as 'e1000' or 'sd_mod'.
If no MODULE arguments are given, they are read from stdin.
Options:
-d|--depmod=PATH depmod executable to call (default: autodetected)
-b|--basedir=DIR base directory to look for modules, default: /
-o|--outputdir=PATH output directory, CONTENTS WILL BE OVERWRITTEN
default: temporary directory under ${TMPDIR:-/tmp}
-k|--kver=VERSION kernel version to run for
default: autodetected from installed kernel
-E|--symvers FILE symvers file for depmod -E (default: autodetected)
-F|--filesyms FILE System.map file for depmod -F (default: autodetected)
-K|--keep do not clean up results
-h|--help print this help
EOF
}

OPTIONS=b:k:E:d:o:F:Kh
LONGOPTS=basedir:,kver:,symvers:,depmod:,outputdir:,filesyms:,keep,help
# shellcheck disable=SC2207
ARGS=$(getopt -o "$OPTIONS" -l "$LONGOPTS" -- "$@")
eval set -- "$ARGS"

while [[ $# -gt 0 ]]; do
case $1 in
-d|--depmod)
shift
DEPMOD=$1
;;
-b|--basedir)
shift
BASEDIR=$1
;;
-k|--kver)
shift
KVER=$1
;;
-E|--symvers)
shift
SYMVERS=$1
;;
-F|--filesyms)
shift
FILESYMS=$1
;;
-o|--outputdir)
shift
OUTPUTDIR=$1
;;
-K|--keep)
KEEP=y
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
esac
shift
done
MODULES=("$@")

find_glob() {
local files

# shellcheck disable=SC2206
files=($1)
case "${files[0]}" in
"$1")
;;
*)
echo "${files[0]}"
;;
esac
}

find_installed_kernel() {
local kernel
kernel=$(find_glob "$BASEDIR/lib/modules/*/modules.dep")
[[ "$kernel" ]] || {
echo "$ME: no installed kernel found" >&2
return
}
kernel=${kernel%/*}
kernel=${kernel##*/}
echo "$ME: found kernel $kernel" >&2
echo "$kernel"
}

find_symvers() {
local symvers

symvers=$(find_glob "$BASEDIR/boot/symvers-$1*")
[[ ! "$symvers" ]] || echo "$symvers"
}

find_filesyms() {
local fsyms

fsyms=$(find_glob "$BASEDIR/boot/System.map-$1*")
[[ ! "$fsyms" ]] || echo "$fsyms"
}

[[ ! "$BASEDIR" ]] || BASEDIR=$(cd "$BASEDIR" && echo "$PWD")

[[ "$DEPMOD" ]] || {
for _x in "${0%/*}/../depmod" "${0%/*}/../tools/depmod" \
/usr/local/sbin/depmod /sbin/depmod /usr/sbin/depmod; do
[[ -f "$_x" && -x "$_x" ]] && {
DEPMOD=$_x
break
}
done
}

[[ "$DEPMOD" && -f "$DEPMOD" && -x "$DEPMOD" ]]
echo "$ME: using DEPMOD=$DEPMOD" >&2
if [[ "$OUTPUTDIR" ]]; then
[[ -d "$OUTPUTDIR" ]] || {
mkdir -p "$OUTPUTDIR"
# shellcheck disable=SC2016
[[ "$KEEP" ]] || CLEANUP='rm -rf "$OUTPUTDIR";'"$CLEANUP"
}
else
OUTPUTDIR=$(mktemp -d "${TMPDIR:-/tmp}/$ME.XXXXXX")
[[ "$OUTPUTDIR" ]]
# shellcheck disable=SC2016
[[ "$KEEP" ]] || CLEANUP='rm -rf "$OUTPUTDIR";'"$CLEANUP"
fi

OUTPUTDIR=$(cd "$OUTPUTDIR" && echo "$PWD")

[[ "$KVER" ]] || KVER=$(find_installed_kernel)
[[ "$KVER" && -d "$BASEDIR/lib/modules/$KVER" ]]

[[ "$SYMVERS" ]] || SYMVERS=$(find_symvers "$KVER")
[[ "$FILESYMS" ]] || FILESYMS=$(find_filesyms "$KVER")

[[ ("$SYMVERS" && -f "$SYMVERS") || ("$FILESYMS" && -f "$FILESYMS") ]]

# depmod can't handle compressed symvers
case $SYMVERS in
*.gz)
gzip -dc "$SYMVERS" >"${TMPDIR:-/tmp}/symvers-$KVER"
SYMVERS="${TMPDIR:-/tmp}/symvers-$KVER"
# shellcheck disable=SC2016
CLEANUP='rm -f "$SYMVERS";'"$CLEANUP"
;;
esac

[[ ! "$SYMVERS" ]] || FILESYMS=
echo "$ME: symbols resolved with ${SYMVERS:+-E "$SYMVERS"}${FILESYMS:+-F "$FILESYMS"}" >&2

[[ "${#MODULES[@]}" -gt 0 ]] || {
echo "$ME: reading modules from stdin ..." >&2
mapfile -t MODULES < <(sed -E 's/\s+/\n/g')
}
[[ "${#MODULES[@]}" -gt 0 ]]

find_module() {
local f
f=$(cd "$2" && find . -name "$1.ko*" -type f -print -quit)
echo "${f#./}"
}

copy_results_to() {
rm -rf "${OUTPUTDIR:?}/$1"
mkdir -p "$OUTPUTDIR/$1"
cp -a "$WORKDIR/lib/modules/$KVER/modules".* "$OUTPUTDIR/$1"
}

restore_results_from() {
cp -a "$OUTPUTDIR/$1/"* "$WORKDIR/lib/modules/$KVER/"
}

delete_modules() {
pushd "$WORKDIR/lib/modules/$KVER" &>/dev/null
rm -f "${!EXTRAMODS[@]}"
popd &>/dev/null
}

restore_deleted_modules() {
pushd "$CLONEDIR/lib/modules/$KVER" &>/dev/null
echo "${!EXTRAMODS[@]}" | sed -E 's/\s+/\n/g' | \
cpio -p --link "$WORKDIR/lib/modules/$KVER" 2>/dev/null
popd &>/dev/null
}

check_results_for() {
local ref
ref=${2:-reference}
if diff -ruq "$OUTPUTDIR/$ref" "$OUTPUTDIR/$1"; then
echo "$ME: $1: OK" >&2
else
echo "$ME: depmod output for in $OUTPUTDIR/$1 differs from $OUTPUTDIR/$ref" >&2
: $((ERR++))
fi
}

# MODPATHS holds the full module paths relative to the module directory,
# this is the same format as in modules.dep
# REGEX will be used to grep modules.dep for any of these paths occurring
# in the dependency list of some module (DEPENDS below).
MODPATHS=()
for x in "${!MODULES[@]}"; do
[[ "$x" ]] || continue
mp=$(find_module "${MODULES[$x]}" "$BASEDIR/lib/modules/$KVER")
if [[ "$mp" ]]; then
MODPATHS["$x"]=$mp
REGEX="$REGEX|${mp//./\\.}"
else
echo "$ME: module ${MODULES[$x]} not found" >&2
fi
done
REGEX=${REGEX#|}

# First, clone the module directory
CLONEDIR=$OUTPUTDIR/clone
[[ -f "$CLONEDIR/lib/modules/$KVER/modules.dep" ]] || {
rm -rf "$CLONEDIR/lib/modules"
mkdir -p "$CLONEDIR/lib/modules"
cp -r "$BASEDIR/lib/modules/$KVER" "$CLONEDIR/lib/modules"

echo "$ME: running depmod over entire tree"
time "$DEPMOD" $DEPMOD_OPTS -b "$CLONEDIR" -e ${SYMVERS:+-E "$SYMVERS"} ${FILESYMS:+-F "$FILESYMS"} "$KVER"
}

# With modules.dep in place, find all modules that depend on MODULES.
mapfile -t DEPENDS < <(sed -nE 's,:.*\<('"$REGEX"')\>.*$,,p' "$CLONEDIR/lib/modules/$KVER/modules.dep")

# EXTRAMODS holds the paths for MODULES and all their dependencies
# use associative array to avoid duplicates
declare -A EXTRAMODS
for x in "${MODPATHS[@]}" "${DEPENDS[@]}"; do
EXTRAMODS["$x"]=1
done

# Copy clone work dir and save the depmod results in "reference" dir
WORKDIR=$OUTPUTDIR/work
rm -rf "$WORKDIR/lib/modules"
mkdir -p "$WORKDIR/lib/modules"
cp -rl "$CLONEDIR/lib/modules/$KVER" "$WORKDIR/lib/modules/$KVER"
copy_results_to reference

# Delete EXTRAMODS, and run depmod again (pretend system state before adding EXTRAMODS)
# Save results in "orig"
delete_modules
echo "$ME: running full depmod after module removal"
time "$DEPMOD" $DEPMOD_OPTS -b "$WORKDIR" -e ${SYMVERS:+-E "$SYMVERS"} ${FILESYMS:+-F "$FILESYMS"} "$KVER"
copy_results_to orig

# 1. Restore deleted modules, and run depmod --incremental
# Save results in "incremental", and compare to "reference"
restore_deleted_modules
echo "$ME: running depmod --incremental (${#EXTRAMODS[@]} modules added)"
time "$DEPMOD" $DEPMOD_OPTS -b "$WORKDIR" -I -e ${SYMVERS:+-E "$SYMVERS"} ${FILESYMS:+-F "$FILESYMS"} "$KVER" "${!EXTRAMODS[@]}"
copy_results_to incremental
check_results_for incremental

# 2. Same thing, but with depmod --incremental --all
# Save results in "incremental-all", and compare to "reference"
restore_results_from orig
echo "$ME: running depmod --incremental --all"
time "$DEPMOD" $DEPMOD_OPTS -b "$WORKDIR" -I -a -e ${SYMVERS:+-E "$SYMVERS"} ${FILESYMS:+-F "$FILESYMS"} "$KVER"
copy_results_to incremental-all
check_results_for incremental-all

# 3. Delete modules again, and test depmod --incremental --all for removal
# Save results in "deleted", and compare to "orig"
delete_modules
echo "$ME: running depmod --incremental --all after module removal"
# this will spit out lots of "module xyz not found" error messages
time "$DEPMOD" $DEPMOD_OPTS -b "$WORKDIR" -I -a -e ${SYMVERS:+-E "$SYMVERS"} ${FILESYMS:+-F "$FILESYMS"} "$KVER" 2>/dev/null
copy_results_to deleted
check_results_for deleted orig

exit "$ERR"

0 comments on commit 6bf3c3d

Please sign in to comment.