Merge branch 'selftests-rds-log-collection-tap-compliance-and-cleanups'

Allison Henderson says:

====================
selftests: rds: Log collection, TAP compliance and cleanups

This series is a set of bug fixes and improvements for the rds
selftests.

Patch 1 bumps the kselftest timeout from 400s to 800s. The original
limit was developed against a lean config, but the kselftest harness
counts boot time and gcov log collection against the limit, so a
default config with gcov enabled needs more headroom.

Patch 2 corrects some typos in the run.sh USAGE string and removes an
unused "-g" flag.

Patch 3 silences a handful of pylint warnings in test.py: it adds a
module docstring, suppresses the warnings tied to the sys.path.append
import trick, marks the long lived tcpdump Popen with disable-next
consider-using-with, and drops unused exception variables from two
BlockingIOError except clauses.

Patch 4 adds a -t flag to run.sh so the timeout can be overridden
if needed.

Patch 5 adds a RDS_LOG_DIR environment variable that specifies where
logs should be stored, or skips log collection if left unset

Patch 6 adds a SUDO_USER environment variable that sets the user
for tcpdump --relinquish-privileges.  This avoid the permissions
drop that would leave pcaps empty on 9pfs since 9p does not
support chown

Patch 7 removes the initial tmp tcpdumps and instead saves the pcaps
directly to the logdir if it is set.

Patch 8 hoists the tcpdump shutdown into a helper and calls it from the
timeout signal handler so that the processes are properly terminated
and dumps are flushed

Patch 9 fixes gcov collection by ensuring debugfs is mounted, and
specifying the --root folder so that gcov can still find the kernel
source when it is run from the ksft test directory.

Patch 10 makes the test output TAP compliant so the kselftest runner
parses results correctly.
====================

Link: https://patch.msgid.link/20260504054143.4027538-1-achender@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Jakub Kicinski
2026-05-05 19:19:56 -07:00
4 changed files with 146 additions and 70 deletions

View File

@@ -12,10 +12,12 @@ kernel may optionally be configured to omit the coverage report as well.
USAGE:
run.sh [-d logdir] [-l packet_loss] [-c packet_corruption]
[-u packet_duplcate]
[-u packet_duplicate] [-t timeout]
OPTIONS:
-d Log directory. Defaults to tools/testing/selftests/net/rds/rds_logs
-d Log directory. If set, logs will be stored in the
given dir, or skipped if unset. Log dir can also be
set through the RDS_LOG_DIR env variable
-l Simulates a percentage of packet loss
@@ -23,6 +25,24 @@ OPTIONS:
-u Simulates a percentage of packet duplication.
-t Test timeout. Defaults to tools/testing/selftests/net/rds/settings
ENV VARIABLES:
RDS_LOG_DIR Log directory. If set, logs will be stored in
the given dir, or skipped if unset. Log dir
can also be set with the -d flag.
Use with --rwdir on the CI path to retain logs after
test compleation. Log dir end point must be within
the specified --rwdir path for logs to persist on
the host.
SUDO_USER The user name that should be used for tcpdump
--relinquish-privileges. Set this to a user
belonging to the sudoers group to avoid drop
privilege errors with the vng 9p filesystem
which may result in empty pcaps
EXAMPLE:
# Create a suitable gcov enabled .config
@@ -39,6 +59,8 @@ EXAMPLE:
# launch the tests in a VM
vng -v --rwdir ./ --run . --user root --cpus 4 -- \
"export PYTHONPATH=tools/testing/selftests/net/; tools/testing/selftests/net/rds/run.sh"
"export PYTHONPATH=tools/testing/selftests/net/; \
export SUDO_USER=example_user; \
export RDS_LOG_DIR=tools/testing/selftests/net/rds/rds_logs; \
tools/testing/selftests/net/rds/run.sh"
An HTML coverage report will be output in tools/testing/selftests/net/rds/rds_logs/coverage/.

View File

@@ -150,28 +150,30 @@ check_env()
fi
}
LOG_DIR="$current_dir"/rds_logs
PLOSS=0
PCORRUPT=0
PDUP=0
LOG_DIR="${RDS_LOG_DIR:-}"
TIMEOUT=$timeout
GENERATE_GCOV_REPORT=1
while getopts "d:l:c:u:" opt; do
FLAGS=()
while getopts "d:l:c:u:t:" opt; do
case ${opt} in
d)
LOG_DIR=${OPTARG}
;;
l)
PLOSS=${OPTARG}
FLAGS+=("-l" "${OPTARG}")
;;
c)
PCORRUPT=${OPTARG}
FLAGS+=("-c" "${OPTARG}")
;;
t)
TIMEOUT=${OPTARG}
;;
u)
PDUP=${OPTARG}
FLAGS+=("-u" "${OPTARG}")
;;
:)
echo "USAGE: run.sh [-d logdir] [-l packet_loss] [-c packet_corruption]" \
"[-u packet_duplcate] [-g]"
"[-u packet_duplicate] [-t timeout]"
exit 1
;;
?)
@@ -181,47 +183,63 @@ while getopts "d:l:c:u:" opt; do
esac
done
check_env
check_conf
check_gcov_conf
TRACE_CMD=()
if [[ -n "$LOG_DIR" ]]; then
rm -fr "$LOG_DIR"
FLAGS+=("-d" "$LOG_DIR")
rm -fr "$LOG_DIR"
TRACE_FILE="${LOG_DIR}/rds-strace.txt"
COVR_DIR="${LOG_DIR}/coverage/"
mkdir -p "$LOG_DIR"
mkdir -p "$COVR_DIR"
TRACE_FILE="${LOG_DIR}/rds-strace.txt"
COVR_DIR="${LOG_DIR}/coverage/"
mkdir -p "$LOG_DIR"
mkdir -p "$COVR_DIR"
echo "#Traces will be logged to ${TRACE_FILE}"
rm -f "$TRACE_FILE"
TRACE_CMD=(strace -T -tt -o "${TRACE_FILE}")
fi
set +e
echo running RDS tests...
echo Traces will be logged to "$TRACE_FILE"
rm -f "$TRACE_FILE"
strace -T -tt -o "$TRACE_FILE" python3 "$(dirname "$0")/test.py" \
--timeout "$timeout" -d "$LOG_DIR" -l "$PLOSS" -c "$PCORRUPT" -u "$PDUP"
echo "#running RDS tests..."
"${TRACE_CMD[@]}" python3 "$(dirname "$0")/test.py" "${FLAGS[@]}" -t "$TIMEOUT"
test_rc=$?
dmesg > "${LOG_DIR}/dmesg.out"
if [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then
echo saving coverage data...
if [[ -n "$LOG_DIR" ]]; then
dmesg > "${LOG_DIR}/dmesg.out"
fi
if [[ -n "$LOG_DIR" ]] && [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then
echo "# saving coverage data..."
# Ensure debugfs is mounted before reading gcov data.
if ! mountpoint -q /sys/kernel/debug 2>/dev/null; then
mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null || true
fi
(set +x; cd /sys/kernel/debug/gcov; find ./* -name '*.gcda' | \
while read -r f
do
cat < "/sys/kernel/debug/gcov/$f" > "/$f"
done)
echo running gcovr...
echo "# running gcovr..."
gcovr -s --html-details --gcov-executable "$GCOV_CMD" --gcov-ignore-parse-errors \
-o "${COVR_DIR}/gcovr" "${ksrc_dir}/net/rds/"
--root "${ksrc_dir}" -o "${COVR_DIR}/gcovr" "${ksrc_dir}/net/rds/" \
> "${LOG_DIR}/gcovr.log" 2>&1
echo "# gcovr log: ${LOG_DIR}/gcovr.log"
else
echo "Coverage report will be skipped"
echo "# Coverage report will be skipped"
fi
if [ "$test_rc" -eq 0 ]; then
echo "PASS: Test completed successfully"
echo "# PASS: Test completed successfully"
else
echo "FAIL: Test failed"
echo "# FAIL: Test failed"
fi
exit "$test_rc"

View File

@@ -1 +1 @@
timeout=400
timeout=800

View File

@@ -1,5 +1,8 @@
#! /usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
This module provides functional testing for the net/rds component.
"""
import argparse
import ctypes
@@ -11,13 +14,14 @@ import signal
import socket
import subprocess
import sys
import tempfile
import shutil
# Allow utils module to be imported from different directory
this_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(this_dir, "../"))
from lib.py.utils import ip
# pylint: disable-next=wrong-import-position,import-error,no-name-in-module
from lib.py.utils import ip # noqa: E402
# pylint: disable-next=wrong-import-position,import-error,no-name-in-module
from lib.py.ksft import ksft_pr # noqa: E402
libc = ctypes.cdll.LoadLibrary('libc.so.6')
setns = libc.setns
@@ -57,7 +61,7 @@ def netns_socket(netns, *sock_args):
# send resulting socket to parent
socket.send_fds(u0, [], [sock.fileno()])
sys.exit(0)
os._exit(0)
# receive socket from child
_, fds, _, _ = socket.recv_fds(u1, 0, 1)
@@ -66,11 +70,30 @@ def netns_socket(netns, *sock_args):
u1.close()
return socket.fromfd(fds[0], *sock_args)
def stop_pcaps():
"""Stop tcpdump processes.
We use pop() here to drain the list in the event that the test
completes after the signal handler is fired. List will be empty
if logdir is not set
"""
ksft_pr("Stopping network packet captures")
while tcpdump_procs:
proc = tcpdump_procs.pop()
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
def signal_handler(_sig, _frame):
"""
Test timed out signal handler
"""
print('Test timed out')
ksft_pr("Test timed out")
stop_pcaps()
print("not ok 1 rds selftest")
sys.exit(1)
#Parse out command line arguments. We take an optional
@@ -78,8 +101,8 @@ def signal_handler(_sig, _frame):
parser = argparse.ArgumentParser(description="init script args",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-d", "--logdir", action="store",
help="directory to store logs", default="/tmp")
parser.add_argument('--timeout', help="timeout to terminate hung test",
help="directory to store logs", default=None)
parser.add_argument('-t', '--timeout', help="timeout to terminate hung test",
type=int, default=0)
parser.add_argument('-l', '--loss', help="Simulate tcp packet loss",
type=int, default=0)
@@ -124,15 +147,22 @@ ip(f"-n {NET1} route add {addrs[0][0]}/32 dev {VETH1}")
# and communicating by doing a single ping
ip(f"netns exec {NET0} ping -c 1 {addrs[1][0]}")
# Start a packet capture on each network
tcpdump_procs = []
for net in [NET0, NET1]:
pcap = logdir+'/'+net+'.pcap'
fd, pcap_tmp = tempfile.mkstemp(suffix=".pcap", prefix=f"{net}-", dir="/tmp")
p = subprocess.Popen(
['ip', 'netns', 'exec', net,
'/usr/sbin/tcpdump', '-i', 'any', '-w', pcap_tmp])
tcpdump_procs.append((p, pcap_tmp, pcap, fd))
# Start a packet capture on each network
if logdir is not None:
for net in [NET0, NET1]:
pcap = logdir+'/'+net+'.pcap'
tcpdump_cmd = ['ip', 'netns', 'exec', net, '/usr/sbin/tcpdump']
sudo_user = os.environ.get('SUDO_USER')
if sudo_user:
tcpdump_cmd.extend(['-Z', sudo_user])
tcpdump_cmd.extend(['-i', 'any', '-w', pcap])
# pylint: disable-next=consider-using-with
p = subprocess.Popen(tcpdump_cmd,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
tcpdump_procs.append(p)
# simulate packet loss, duplication and corruption
for net, iface in [(NET0, VETH0), (NET1, VETH1)]:
@@ -140,6 +170,9 @@ for net, iface in [(NET0, VETH0), (NET1, VETH1)]:
corrupt {PACKET_CORRUPTION} loss {PACKET_LOSS} duplicate \
{PACKET_DUPLICATE}")
print("TAP version 13")
print("1..1")
# add a timeout
if args.timeout > 0:
signal.alarm(args.timeout)
@@ -178,7 +211,7 @@ nr_recv = 0
while nr_send < NUM_PACKETS:
# Send as much as we can without blocking
print("sending...", nr_send, nr_recv)
ksft_pr("sending...", nr_send, nr_recv)
while nr_send < NUM_PACKETS:
send_data = hashlib.sha256(
f'packet {nr_send}'.encode('utf-8')).hexdigest().encode('utf-8')
@@ -192,7 +225,7 @@ while nr_send < NUM_PACKETS:
send_hashes.setdefault((sender.fileno(), receiver.fileno()),
hashlib.sha256()).update(f'<{send_data}>'.encode('utf-8'))
nr_send = nr_send + 1
except BlockingIOError as e:
except BlockingIOError:
break
except OSError as e:
if e.errno in [errno.ENOBUFS, errno.ECONNRESET, errno.EPIPE]:
@@ -200,7 +233,7 @@ while nr_send < NUM_PACKETS:
raise
# Receive as much as we can without blocking
print("receiving...", nr_send, nr_recv)
ksft_pr("receiving...", nr_send, nr_recv)
while nr_recv < nr_send:
for fileno, eventmask in ep.poll():
receiver = fileno_to_socket[fileno]
@@ -214,7 +247,7 @@ while nr_send < NUM_PACKETS:
receiver.fileno()), hashlib.sha256()).update(
f'<{recv_data}>'.encode('utf-8'))
nr_recv = nr_recv + 1
except BlockingIOError as e:
except BlockingIOError:
break
# exercise net/rds/tcp.c:rds_tcp_sysctl_reset()
@@ -222,7 +255,7 @@ while nr_send < NUM_PACKETS:
ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_rcvbuf=10000")
ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_sndbuf=10000")
print("done", nr_send, nr_recv)
ksft_pr("done", nr_send, nr_recv)
# the Python socket module doesn't know these
RDS_INFO_FIRST = 10000
@@ -245,31 +278,34 @@ for s in sockets:
# ignore
pass
print(f"getsockopt(): {nr_success}/{nr_error}")
print("Stopping network packet captures")
for p, pcap_tmp, pcap, fd in tcpdump_procs:
p.terminate()
p.wait()
os.close(fd)
shutil.move(pcap_tmp, pcap)
ksft_pr(f"getsockopt(): {nr_success}/{nr_error}")
stop_pcaps()
# We're done sending and receiving stuff, now let's check if what
# we received is what we sent.
ret = 0
for (sender, receiver), send_hash in send_hashes.items():
recv_hash = recv_hashes.get((sender, receiver))
if recv_hash is None:
print("FAIL: No data received")
sys.exit(1)
ksft_pr("FAIL: No data received")
ret = 1
break
if send_hash.hexdigest() != recv_hash.hexdigest():
print("FAIL: Send/recv mismatch")
print("hash expected:", send_hash.hexdigest())
print("hash received:", recv_hash.hexdigest())
sys.exit(1)
ksft_pr("FAIL: Send/recv mismatch")
ksft_pr("hash expected:", send_hash.hexdigest())
ksft_pr("hash received:", recv_hash.hexdigest())
ret = 1
break
print(f"{sender}/{receiver}: ok")
ksft_pr(f"{sender}/{receiver}: ok")
print("Success")
sys.exit(0)
if ret == 0:
ksft_pr("Success")
print("ok 1 rds selftest")
else:
print("not ok 1 rds selftest")
ksft_pr(f"Totals: pass:{1-ret} fail:{ret} skip:0")
sys.exit(ret)