summaryrefslogtreecommitdiffstats
path: root/lib/testing/test_runner.sh
diff options
context:
space:
mode:
Diffstat (limited to 'lib/testing/test_runner.sh')
-rwxr-xr-xlib/testing/test_runner.sh591
1 files changed, 591 insertions, 0 deletions
diff --git a/lib/testing/test_runner.sh b/lib/testing/test_runner.sh
new file mode 100755
index 0000000..726b902
--- /dev/null
+++ b/lib/testing/test_runner.sh
@@ -0,0 +1,591 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Script to setup and manage tests for xdp-tools.
+# Based on the test-env script from xdp-tutorial.
+#
+# Author: Toke Høiland-Jørgensen (toke@redhat.com)
+# Date: 26 May 2020
+# Copyright (c) 2020 Red Hat
+
+set -o errexit
+set -o nounset
+umask 077
+
+TEST_PROG_DIR="${TEST_PROG_DIR:-$(dirname "${BASH_SOURCE[0]}")}"
+SETUP_SCRIPT="$TEST_PROG_DIR/setup-netns-env.sh"
+TEST_CONFIG="$TEST_PROG_DIR/test_config.sh"
+IP6_SUBNET=fc42:dead:cafe # must have exactly three :-separated elements
+IP6_PREFIX_SIZE=64 # Size of assigned prefixes
+IP6_FULL_PREFIX_SIZE=48 # Size of IP6_SUBNET
+IP4_SUBNET=10.11
+IP4_PREFIX_SIZE=24 # Size of assigned prefixes
+IP4_FULL_PREFIX_SIZE=16 # Size of IP4_SUBNET
+VLAN_IDS=(1 2)
+GENERATED_NAME_PREFIX="xdptest"
+ALL_TESTS=""
+VERBOSE_TESTS=${V:-0}
+
+NEEDED_TOOLS="capinfos ethtool ip ping sed tc tcpdump timeout nc tshark"
+MAX_NAMELEN=15
+
+if [ -f "$TEST_CONFIG" ]; then
+ source "$TEST_CONFIG"
+fi
+
+if command -v ping6 >/dev/null 2>&1; then
+ PING6=ping6
+else
+ PING6=ping
+fi
+
+# Odd return value for skipping, as only 0-255 is valid.
+SKIPPED_TEST=249
+
+# Global state variables that will be set by options etc below
+GENERATE_NEW=0
+CLEANUP_FUNC=
+STATEDIR=
+STATEFILE=
+CMD=
+NS=
+LEGACY_IP=1
+USE_VLAN=0
+RUN_ON_INNER=0
+
+# State variables that are written to and read from statefile
+STATEVARS=(IP6_PREFIX IP4_PREFIX
+ INSIDE_IP6 INSIDE_IP4 INSIDE_MAC
+ OUTSIDE_IP6 OUTSIDE_IP4 OUTSIDE_MAC
+ ENABLE_IPV4 ENABLE_VLAN)
+IP6_PREFIX=
+IP4_PREFIX=
+INSIDE_IP6=
+INSIDE_IP4=
+INSIDE_MAC=
+OUTSIDE_IP6=
+OUTSIDE_IP4=
+OUTSIDE_MAC=
+ENABLE_IPV4=0
+ENABLE_VLAN=0
+
+is_trace_attach_supported()
+{
+ if [[ -z "${TRACE_ATTACH_SUPPORT:-}" ]]; then
+ [ -f "$STATEDIR/trace_attach_support" ] && \
+ TRACE_ATTACH_SUPPORT=$(< "$STATEDIR/trace_attach_support")
+
+ if [[ -z "${TRACE_ATTACH_SUPPORT:-}" ]]; then
+ RESULT=$($XDP_LOADER load -v "$NS" "$TEST_PROG_DIR/xdp_pass.o" 2>&1)
+ PID=$(start_background "$XDPDUMP -i $NS")
+ RESULT=$(stop_background "$PID")
+ if [[ "$RESULT" == *"The kernel does not support fentry function attach"* ]]; then
+ TRACE_ATTACH_SUPPORT="false"
+ else
+ TRACE_ATTACH_SUPPORT="true"
+ fi
+ echo "$TRACE_ATTACH_SUPPORT" > "$STATEDIR/trace_attach_support"
+ $XDP_LOADER unload "$NS" --all
+ fi
+ fi
+
+ if [[ "$TRACE_ATTACH_SUPPORT" == "true" ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+is_multiprog_supported()
+{
+ if [[ -z "${MULTIPROG_SUPPORT:-}" ]]; then
+ RESULT=$($XDP_LOADER load -v "$NS" "$TEST_PROG_DIR/xdp_pass.o" 2>&1)
+ if [[ "$RESULT" == *"Compatibility check for dispatcher program failed"* ]]; then
+ MULTIPROG_SUPPORT="false"
+ else
+ MULTIPROG_SUPPORT="true"
+ fi
+ $XDP_LOADER unload "$NS" --all
+ fi
+
+ if [[ "$MULTIPROG_SUPPORT" == "true" ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+is_progmap_supported()
+{
+ if [[ -z "${PROGMAP_SUPPORT:-}" ]]; then
+ RESULT=$(timeout -s INT 1 $XDP_BENCH redirect-cpu "$NS" -c 0 -r drop -vv 2>&1)
+ if [[ "$RESULT" == *"Create CPU entry failed: Cannot allocate memory"* ]]; then
+ PROGMAP_SUPPORT="false"
+ else
+ PROGMAP_SUPPORT="true"
+ fi
+ fi
+
+ if [[ "$PROGMAP_SUPPORT" == "true" ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+skip_if_missing_veth_rxq()
+{
+ if ! ethtool -l $NS >/dev/null 2>&1; then
+ exit "$SKIPPED_TEST"
+ fi
+}
+
+skip_if_missing_cpumap_attach()
+{
+ if ! $TEST_PROG_DIR/test-tool probe cpumap-prog; then
+ exit "$SKIPPED_TEST"
+ fi
+}
+
+skip_if_missing_kernel_symbol()
+{
+ if ! grep -q "$1" /proc/kallsyms; then
+ exit "$SKIPPED_TEST"
+ fi
+}
+
+skip_if_legacy_fallback()
+{
+ if ! is_multiprog_supported; then
+ exit "$SKIPPED_TEST"
+ fi
+}
+
+skip_if_missing_trace_attach()
+{
+ if ! is_trace_attach_supported; then
+ exit "$SKIPPED_TEST"
+ fi
+}
+
+die()
+{
+ echo "$1" >&2
+ exit 1
+}
+
+start_background()
+{
+ local TMP_FILE="${STATEDIR}/tmp_proc_$$_$RANDOM"
+ setsid bash -c "$*" &> ${TMP_FILE} &
+ local PID=$!
+ sleep 2 # Wait to make sure the command is executed in the background
+
+ mv "$TMP_FILE" "${STATEDIR}/proc/${PID}" >& /dev/null
+
+ echo "$PID"
+}
+
+start_background_no_stderr()
+{
+ local TMP_FILE="${STATEDIR}/tmp_proc_$$_$RANDOM"
+ setsid bash -c "$*" 1> ${TMP_FILE} 2>/dev/null &
+ local PID=$!
+ sleep 2 # Wait to make sure the command is executed in the background
+
+ mv "$TMP_FILE" "${STATEDIR}/proc/${PID}" >& /dev/null
+
+ echo "$PID"
+}
+
+start_background_ns_devnull()
+{
+ get_nsname && ensure_nsname "$NS"
+
+ local TMP_FILE="${STATEDIR}/tmp_proc_$$_$RANDOM"
+ setsid ip netns exec "$NS" env TESTENV_NAME="$NS" "$SETUP_SCRIPT" bash -c "$*" 1>/dev/null 2>${TMP_FILE} &
+ local PID=$!
+ sleep 2 # Wait to make sure the command is executed in the background
+
+ mv "$TMP_FILE" "${STATEDIR}/proc/${PID}" >& /dev/null
+ echo $PID
+}
+
+stop_background()
+{
+ local PID=$1
+
+ local OUTPUT_FILE="${STATEDIR}/proc/${PID}"
+ if kill -SIGINT "-$PID" 2>/dev/null; then
+ sleep 2 # Wait to make sure the buffer is flushed after the shutdown
+ kill -SIGTERM "-$PID" 2>/dev/null && sleep 1 # just in case SIGINT was not enough
+ fi
+
+ if [ -f "$OUTPUT_FILE" ]; then
+ cat "$OUTPUT_FILE"
+ rm "$OUTPUT_FILE" >& /dev/null
+ fi
+}
+
+check_prereq()
+{
+ local max_locked_mem=$(ulimit -l)
+
+ for t in $NEEDED_TOOLS; do
+ command -v "$t" > /dev/null || die "Missing required tool: $t"
+ done
+
+ if [ "$EUID" -ne "0" ]; then
+ die "This script needs root permissions to run."
+ fi
+
+ STATEDIR="$(mktemp -d --tmpdir=${TMPDIR:-/tmp} --suffix=.xdptest)"
+ if [ $? -ne 0 ]; then
+ die "Unable to create state dir in $TMPDIR"
+ fi
+ mkdir ${STATEDIR}/proc
+
+ if [ "$max_locked_mem" != "unlimited" ]; then
+ ulimit -l unlimited || die "Unable to set ulimit"
+ fi
+}
+
+get_nsname()
+{
+ local GENERATE=${1:-1}
+
+ if [ -z "$NS" ]; then
+ [ -f "$STATEDIR/current" ] && NS=$(< "$STATEDIR/current")
+
+ if [ "$GENERATE" -eq "1" ] && [ -z "$NS" ] || [ "$GENERATE_NEW" -eq "1" ]; then
+ NS="$GENERATED_NAME_PREFIX"
+ while [ -e "$STATEDIR/${NS}.state" ]; do
+ NS=$(printf "%s-%04x" "$GENERATED_NAME_PREFIX" $RANDOM)
+ done
+ fi
+ fi
+
+ if [ "${#NS}" -gt "$MAX_NAMELEN" ]; then
+ die "Environment name '$NS' is too long (max $MAX_NAMELEN)"
+ fi
+
+ STATEFILE="$STATEDIR/${NS}.state"
+}
+
+ensure_nsname()
+{
+ [ -z "$NS" ] && die "No environment selected; use --name to select one or 'setup' to create one"
+ [ -e "$STATEFILE" ] || die "Environment for $NS doesn't seem to exist"
+
+ echo "$NS" > "$STATEDIR/current"
+
+ read_statefile
+}
+
+get_num()
+{
+ local num=1
+ if [ -f "$STATEDIR/highest_num" ]; then
+ num=$(( 1 + $(< "$STATEDIR/highest_num" )))
+ fi
+
+ echo $num > "$STATEDIR/highest_num"
+ printf "%x" $num
+}
+
+write_statefile()
+{
+ [ -z "$STATEFILE" ] && return 1
+ echo > "$STATEFILE"
+ for var in "${STATEVARS[@]}"; do
+ echo "${var}='$(eval echo '$'$var)'" >> "$STATEFILE"
+ done
+}
+
+read_statefile()
+{
+ local value
+ for var in "${STATEVARS[@]}"; do
+ value=$(source "$STATEFILE"; eval echo '$'$var)
+ eval "$var=\"$value\""
+ done
+}
+
+cleanup_setup()
+{
+ echo "Error during setup, removing partially-configured environment '$NS'" >&2
+ set +o errexit
+ ip netns del "$NS" 2>/dev/null
+ ip link del dev "$NS" 2>/dev/null
+ rm -f "$STATEFILE"
+}
+
+cleanup_teardown()
+{
+ echo "Warning: Errors during teardown, partial environment may be left" >&2
+}
+
+
+cleanup()
+{
+ [ -n "$CLEANUP_FUNC" ] && $CLEANUP_FUNC
+
+ local statefiles=("$STATEDIR"/*.state)
+
+ if [ "${#statefiles[*]}" -eq 1 ] && [ ! -e "${statefiles[0]}" ]; then
+ rm -f "${STATEDIR}/highest_num" "${STATEDIR}/current" \
+ "${STATEDIR}/trace_attach_support"
+ rmdir "$STATEDIR"
+ fi
+}
+
+iface_macaddr()
+{
+ local iface="$1"
+ ip -br link show dev "$iface" | awk '{print $3}'
+}
+
+set_sysctls()
+{
+ local iface="$1"
+ local in_ns="${2:-}"
+ local nscmd=
+
+ [ -n "$in_ns" ] && nscmd="ip netns exec $in_ns"
+ local sysctls=(accept_dad
+ accept_ra
+ mldv1_unsolicited_report_interval
+ mldv2_unsolicited_report_interval)
+
+ for s in ${sysctls[*]}; do
+ $nscmd sysctl -w net.ipv6.conf.$iface.${s}=0 >/dev/null
+ done
+}
+
+get_vlan_prefix()
+{
+ # Split the IPv6 prefix, and add the VLAN ID to the upper byte of the fourth
+ # element in the prefix. This will break if the global prefix config doesn't
+ # have exactly three elements in it.
+ local prefix="$1"
+ local vid="$2"
+ (IFS=:; set -- $prefix; printf "%s:%s:%s:%x::" "$1" "$2" "$3" $(($4 + $vid * 4096)))
+}
+
+setup()
+{
+ get_nsname 1
+
+ [ -e "$STATEFILE" ] && die "Environment for '$NS' already exists"
+
+ local NUM=$(get_num "$NS")
+ local PEERNAME="testl-ve-$NUM"
+ [ -z "$IP6_PREFIX" ] && IP6_PREFIX="${IP6_SUBNET}:${NUM}::"
+ [ -z "$IP4_PREFIX" ] && IP4_PREFIX="${IP4_SUBNET}.$((0x$NUM))."
+
+ INSIDE_IP6="${IP6_PREFIX}2"
+ INSIDE_IP4="${IP4_PREFIX}2"
+ OUTSIDE_IP6="${IP6_PREFIX}1"
+ OUTSIDE_IP4="${IP4_PREFIX}1"
+
+ CLEANUP_FUNC=cleanup_setup
+
+ if ! mount | grep -q /sys/fs/bpf; then
+ mount -t bpf bpf /sys/fs/bpf/
+ fi
+
+ ip netns add "$NS"
+ ip link add dev "$NS" type veth peer name "$PEERNAME"
+ OUTSIDE_MAC=$(iface_macaddr "$NS")
+ INSIDE_MAC=$(iface_macaddr "$PEERNAME")
+ set_sysctls $NS
+
+ ethtool -K "$NS" rxvlan off txvlan off
+ ethtool -K "$PEERNAME" rxvlan off txvlan off
+ ip link set dev "$PEERNAME" netns "$NS"
+ ip link set dev "$NS" up
+ ip addr add dev "$NS" "${OUTSIDE_IP6}/${IP6_PREFIX_SIZE}"
+
+ ip -n "$NS" link set dev "$PEERNAME" name veth0
+ ip -n "$NS" link set dev lo up
+ ip -n "$NS" link set dev veth0 up
+ set_sysctls veth0 "$NS"
+ ip -n "$NS" addr add dev veth0 "${INSIDE_IP6}/${IP6_PREFIX_SIZE}"
+
+ # Prevent neighbour queries on the link
+ ip neigh add "$INSIDE_IP6" lladdr "$INSIDE_MAC" dev "$NS" nud permanent
+ ip -n "$NS" neigh add "$OUTSIDE_IP6" lladdr "$OUTSIDE_MAC" dev veth0 nud permanent
+
+ # Add route for whole test subnet, to make it easier to communicate between
+ # namespaces
+ ip -n "$NS" route add "${IP6_SUBNET}::/$IP6_FULL_PREFIX_SIZE" via "$OUTSIDE_IP6" dev veth0
+
+ if [ "$LEGACY_IP" -eq "1" ]; then
+ ip addr add dev "$NS" "${OUTSIDE_IP4}/${IP4_PREFIX_SIZE}"
+ ip -n "$NS" addr add dev veth0 "${INSIDE_IP4}/${IP4_PREFIX_SIZE}"
+ ip neigh add "$INSIDE_IP4" lladdr "$INSIDE_MAC" dev "$NS" nud permanent
+ ip -n "$NS" neigh add "$OUTSIDE_IP4" lladdr "$OUTSIDE_MAC" dev veth0 nud permanent
+ ip -n "$NS" route add "${IP4_SUBNET}/${IP4_FULL_PREFIX_SIZE}" via "$OUTSIDE_IP4" dev veth0
+ ENABLE_IPV4=1
+ else
+ ENABLE_IPV4=0
+ fi
+
+ if [ "$USE_VLAN" -eq "1" ]; then
+ ENABLE_VLAN=1
+ for vid in "${VLAN_IDS[@]}"; do
+ local vlpx="$(get_vlan_prefix "$IP6_PREFIX" "$vid")"
+ local inside_ip="${vlpx}2"
+ local outside_ip="${vlpx}1"
+ ip link add dev "${NS}.$vid" link "$NS" type vlan id "$vid"
+ ip link set dev "${NS}.$vid" up
+ ip addr add dev "${NS}.$vid" "${outside_ip}/${IP6_PREFIX_SIZE}"
+ ip neigh add "$inside_ip" lladdr "$INSIDE_MAC" dev "${NS}.$vid" nud permanent
+ set_sysctls "${NS}/$vid"
+
+ ip -n "$NS" link add dev "veth0.$vid" link "veth0" type vlan id "$vid"
+ ip -n "$NS" link set dev "veth0.$vid" up
+ ip -n "$NS" addr add dev "veth0.$vid" "${inside_ip}/${IP6_PREFIX_SIZE}"
+ ip -n "$NS" neigh add "$outside_ip" lladdr "$OUTSIDE_MAC" dev "veth0.$vid" nud permanent
+ set_sysctls "veth0/$vid" "$NS"
+ done
+ else
+ ENABLE_VLAN=0
+ fi
+
+ write_statefile
+
+ CLEANUP_FUNC=
+
+ echo "$NS" > "$STATEDIR/current"
+}
+
+teardown()
+{
+ get_nsname && ensure_nsname "$NS"
+
+ CLEANUP_FUNC=cleanup_teardown
+
+ ip link del dev "$NS"
+ ip netns del "$NS"
+ rm -f "$STATEFILE"
+ [ -d "/sys/fs/bpf/$NS" ] && rmdir "/sys/fs/bpf/$NS" || true
+
+ if [ -f "$STATEDIR/current" ]; then
+ local CUR=$(< "$STATEDIR/current" )
+ [[ "$CUR" == "$NS" ]] && rm -f "$STATEDIR/current"
+ fi
+
+ for f in ${STATEDIR}/proc/*; do
+ if [ -f "$f" ]; then
+ local pid="${f/${STATEDIR}\/proc\//}"
+ stop_background "$pid" &> /dev/null || true
+ fi
+ done
+
+ rm -rf "$STATEDIR"
+
+ CLEANUP_FUNC=
+}
+
+ns_exec()
+{
+ get_nsname && ensure_nsname "$NS"
+
+ ip netns exec "$NS" env TESTENV_NAME="$NS" "$SETUP_SCRIPT" "$@"
+}
+
+is_func()
+{
+ type "$1" 2>/dev/null | grep -q 'is a function'
+}
+
+check_run()
+{
+ local ret
+
+ "$@"
+ ret=$?
+ echo "Command '$@' exited with status $ret"
+ echo ""
+ if [ "$ret" -ne "0" ]; then
+ exit $ret
+ fi
+}
+
+exec_test()
+{
+ local testn="$1"
+ local output
+ local ret
+
+ printf " %-30s" "[$testn]"
+ if ! is_func "$testn"; then
+ echo "INVALID"
+ return 1
+ fi
+
+ output=$($testn 2>&1)
+ ret=$?
+ if [ "$ret" -eq "0" ]; then
+ echo "PASS"
+ elif [ "$ret" -eq "$SKIPPED_TEST" ]; then
+ echo "SKIPPED"
+ ret=0
+ else
+ echo "FAIL"
+ fi
+ if [ "$ret" -ne "0" ] || [ "$VERBOSE_TESTS" -eq "1" ]; then
+ echo "$output" | sed 's/^/ /'
+ echo " Test $testn exited with return code: $ret"
+ fi
+ return $ret
+}
+
+run_tests()
+{
+ local TESTS="$*"
+ local ret=0
+ [ -z "$TESTS" ] && TESTS="$ALL_TESTS"
+ setup || return 1
+
+ echo " Running tests from $TEST_DEFINITIONS"
+
+ for testn in $TESTS; do
+ exec_test $testn || ret=1
+ if is_func cleanup_tests; then
+ cleanup_tests || true
+ fi
+ done
+
+ return $ret
+}
+
+usage()
+{
+ echo "Usage: $0 <test_definition_file> [test names]" >&2
+ exit 1
+}
+
+if [ "$EUID" -ne "0" ]; then
+ if command -v sudo >/dev/null 2>&1; then
+ exec sudo env V=${VERBOSE_TESTS} "$0" "$@"
+ else
+ die "Tests should be run as root"
+ fi
+fi
+
+export XDP_FILTER
+export XDP_LOADER
+export XDPDUMP
+
+TEST_DEFINITIONS="${1:-}"
+[ -f "$TEST_DEFINITIONS" ] || usage
+source "$TEST_DEFINITIONS"
+
+TOOL_TESTS_DIR="$(dirname "$TEST_DEFINITIONS")"
+
+shift
+trap teardown EXIT
+check_prereq
+run_tests "$@"