* [PATCH net-next v1 1/2] selftests: rds: Refactor test.py
2026-03-02 5:55 [PATCH net-next v1 0/2] selftests: rds: refactor and expand rds selftests test Allison Henderson
@ 2026-03-02 5:55 ` Allison Henderson
2026-03-02 5:55 ` [PATCH net-next v1 2/2] selftests: rds: Add rds_stress.py Allison Henderson
2026-03-03 1:33 ` [PATCH net-next v1 0/2] selftests: rds: refactor and expand rds selftests test Jakub Kicinski
2 siblings, 0 replies; 8+ messages in thread
From: Allison Henderson @ 2026-03-02 5:55 UTC (permalink / raw)
To: netdev
Cc: linux-kselftest, pabeni, edumazet, rds-devel, kuba, horms,
linux-rdma, allison.henderson
From: Allison Henderson <allison.henderson@oracle.com>
This patch hoists the send and recv logic from test.py into a helper
module rds_basic.py, keeping the namespace and networking configurations
in the calling test.py script. This will allow us to reuse the test.py
infrastructure to add additional rds tests. No functional changes are
added in this initial refactor.
Signed-off-by: Allison Henderson <achender@kernel.org>
---
tools/testing/selftests/net/rds/Makefile | 1 +
tools/testing/selftests/net/rds/rds_basic.py | 183 +++++++++++++++++++
tools/testing/selftests/net/rds/test.py | 169 +----------------
3 files changed, 191 insertions(+), 162 deletions(-)
diff --git a/tools/testing/selftests/net/rds/Makefile b/tools/testing/selftests/net/rds/Makefile
index 762845cc973c..611ed6f2bf91 100644
--- a/tools/testing/selftests/net/rds/Makefile
+++ b/tools/testing/selftests/net/rds/Makefile
@@ -8,6 +8,7 @@ TEST_PROGS := run.sh
TEST_FILES := \
include.sh \
test.py \
+ rds_basic.py \
# end of TEST_FILES
EXTRA_CLEAN := \
diff --git a/tools/testing/selftests/net/rds/rds_basic.py b/tools/testing/selftests/net/rds/rds_basic.py
new file mode 100755
index 000000000000..49c524e2c72a
--- /dev/null
+++ b/tools/testing/selftests/net/rds/rds_basic.py
@@ -0,0 +1,183 @@
+#! /usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+import ctypes
+import errno
+import hashlib
+import os
+import select
+import socket
+import sys
+
+# 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
+
+libc = ctypes.cdll.LoadLibrary('libc.so.6')
+setns = libc.setns
+
+# Helper function for creating a socket inside a network namespace.
+# We need this because otherwise RDS will detect that the two TCP
+# sockets are on the same interface and use the loop transport instead
+# of the TCP transport.
+def netns_socket(netns, *args):
+ """Create a socket inside a network namespace."""
+ u0, u1 = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+
+ child = os.fork()
+ if child == 0:
+ # change network namespace
+ with open(f'/var/run/netns/{netns}', encoding='utf-8') as f:
+ try:
+ setns(f.fileno(), 0)
+ except IOError as ioe:
+ print(ioe.errno)
+ print(ioe)
+
+ # create socket in target namespace
+ s = socket.socket(*args)
+
+ # send resulting socket to parent
+ socket.send_fds(u0, [], [s.fileno()])
+
+ sys.exit(0)
+
+ # receive socket from child
+ _, s, _, _ = socket.recv_fds(u1, 0, 1)
+ os.waitpid(child, 0)
+ u0.close()
+ u1.close()
+ return socket.fromfd(s[0], *args)
+
+def run_test(env):
+ """Run basic RDS selftest.
+
+ env is a dictionary provided by test.py and is expected to contain:
+ - 'addrs': list of (ip, port) tuples matching the sockets
+ - 'netns': list of network namespace names (for sysctl exercises)
+ """
+ addrs = env['addrs'] # [('10.0.0.1', 10000), ('10.0.0.2', 20000)]
+ netns_list = env['netns'] # ['net0', 'net1']
+
+ sockets = [
+ netns_socket(netns_list[0], socket.AF_RDS, socket.SOCK_SEQPACKET),
+ netns_socket(netns_list[1], socket.AF_RDS, socket.SOCK_SEQPACKET),
+ ]
+
+ for s, addr in zip(sockets, addrs):
+ s.bind(addr)
+ s.setblocking(0)
+
+ fileno_to_socket = {
+ s.fileno(): s for s in sockets
+ }
+
+ addr_to_socket = dict(zip(addrs, sockets))
+
+ socket_to_addr = {
+ s: addr for addr, s in zip(addrs, sockets)
+ }
+
+ send_hashes = {}
+ recv_hashes = {}
+
+ ep = select.epoll()
+
+ for s in sockets:
+ ep.register(s, select.EPOLLRDNORM)
+
+ n = 50000
+ nr_send = 0
+ nr_recv = 0
+
+ while nr_send < n:
+ # Send as much as we can without blocking
+ print("sending...", nr_send, nr_recv)
+ while nr_send < n:
+ send_data = hashlib.sha256(
+ f'packet {nr_send}'.encode('utf-8')).hexdigest().encode('utf-8')
+
+ # pseudo-random send/receive pattern
+ sender = sockets[nr_send % 2]
+ receiver = sockets[1 - (nr_send % 3) % 2]
+
+ try:
+ sender.sendto(send_data, socket_to_addr[receiver])
+ send_hashes.setdefault((sender.fileno(), receiver.fileno()),
+ hashlib.sha256()).update(f'<{send_data}>'.encode('utf-8'))
+ nr_send = nr_send + 1
+ except BlockingIOError:
+ break
+ except OSError as e:
+ if e.errno in [errno.ENOBUFS, errno.ECONNRESET, errno.EPIPE]:
+ break
+ raise
+
+ # Receive as much as we can without blocking
+ print("receiving...", nr_send, nr_recv)
+ while nr_recv < nr_send:
+ for fileno, eventmask in ep.poll():
+ receiver = fileno_to_socket[fileno]
+
+ if eventmask & select.EPOLLRDNORM:
+ while True:
+ try:
+ recv_data, address = receiver.recvfrom(1024)
+ sender = addr_to_socket[address]
+ recv_hashes.setdefault((sender.fileno(),
+ receiver.fileno()), hashlib.sha256()).update(
+ f'<{recv_data}>'.encode('utf-8'))
+ nr_recv = nr_recv + 1
+ except BlockingIOError:
+ break
+
+ # exercise net/rds/tcp.c:rds_tcp_sysctl_reset()
+ for net in netns_list:
+ 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)
+
+ # the Python socket module doesn't know these
+ RDS_INFO_FIRST = 10000
+ RDS_INFO_LAST = 10017
+
+ nr_success = 0
+ nr_error = 0
+
+ for s in sockets:
+ for optname in range(RDS_INFO_FIRST, RDS_INFO_LAST + 1):
+ # Sigh, the Python socket module doesn't allow us to pass
+ # buffer lengths greater than 1024 for some reason. RDS
+ # wants multiple pages.
+ try:
+ s.getsockopt(socket.SOL_RDS, optname, 1024)
+ nr_success = nr_success + 1
+ except OSError as ose:
+ nr_error = nr_error + 1
+ if ose.errno == errno.ENOSPC:
+ # ignore
+ pass
+
+ print(f"getsockopt(): {nr_success}/{nr_error}")
+
+ # We're done sending and receiving stuff, now let's check if what
+ # we received is what we sent.
+ 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")
+ return 1
+
+ if send_hash.hexdigest() != recv_hash.hexdigest():
+ print("FAIL: Send/recv mismatch")
+ print("hash expected:", send_hash.hexdigest())
+ print("hash received:", recv_hash.hexdigest())
+ return 1
+
+ print(f"{sender}/{receiver}: ok")
+
+ print("Success")
+ return 0
diff --git a/tools/testing/selftests/net/rds/test.py b/tools/testing/selftests/net/rds/test.py
index 4a7178d11193..0cb060073f6d 100755
--- a/tools/testing/selftests/net/rds/test.py
+++ b/tools/testing/selftests/net/rds/test.py
@@ -2,65 +2,25 @@
# SPDX-License-Identifier: GPL-2.0
import argparse
-import ctypes
-import errno
-import hashlib
import os
-import select
import signal
-import socket
import subprocess
import sys
-import atexit
from pwd import getpwuid
from os import stat
+import rds_basic
# 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
-libc = ctypes.cdll.LoadLibrary('libc.so.6')
-setns = libc.setns
-
net0 = 'net0'
net1 = 'net1'
veth0 = 'veth0'
veth1 = 'veth1'
-# Helper function for creating a socket inside a network namespace.
-# We need this because otherwise RDS will detect that the two TCP
-# sockets are on the same interface and use the loop transport instead
-# of the TCP transport.
-def netns_socket(netns, *args):
- u0, u1 = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET)
-
- child = os.fork()
- if child == 0:
- # change network namespace
- with open(f'/var/run/netns/{netns}') as f:
- try:
- ret = setns(f.fileno(), 0)
- except IOError as e:
- print(e.errno)
- print(e)
-
- # create socket in target namespace
- s = socket.socket(*args)
-
- # send resulting socket to parent
- socket.send_fds(u0, [], [s.fileno()])
-
- sys.exit(0)
-
- # receive socket from child
- _, s, _, _ = socket.recv_fds(u1, 0, 1)
- os.waitpid(child, 0)
- u0.close()
- u1.close()
- return socket.fromfd(s[0], *args)
-
def signal_handler(sig, frame):
print('Test timed out')
sys.exit(1)
@@ -87,7 +47,7 @@ packet_duplicate=str(args.duplicate)+'%'
ip(f"netns add {net0}")
ip(f"netns add {net1}")
-ip(f"link add type veth")
+ip("link add type veth")
addrs = [
# we technically don't need different port numbers, but this will
@@ -137,129 +97,14 @@ if args.timeout > 0:
signal.alarm(args.timeout)
signal.signal(signal.SIGALRM, signal_handler)
-sockets = [
- netns_socket(net0, socket.AF_RDS, socket.SOCK_SEQPACKET),
- netns_socket(net1, socket.AF_RDS, socket.SOCK_SEQPACKET),
-]
-
-for s, addr in zip(sockets, addrs):
- s.bind(addr)
- s.setblocking(0)
-
-fileno_to_socket = {
- s.fileno(): s for s in sockets
-}
-
-addr_to_socket = {
- addr: s for addr, s in zip(addrs, sockets)
+env = {
+ 'addrs': addrs,
+ 'netns': [net0, net1],
}
-socket_to_addr = {
- s: addr for addr, s in zip(addrs, sockets)
-}
-
-send_hashes = {}
-recv_hashes = {}
-
-ep = select.epoll()
-
-for s in sockets:
- ep.register(s, select.EPOLLRDNORM)
-
-n = 50000
-nr_send = 0
-nr_recv = 0
-
-while nr_send < n:
- # Send as much as we can without blocking
- print("sending...", nr_send, nr_recv)
- while nr_send < n:
- send_data = hashlib.sha256(
- f'packet {nr_send}'.encode('utf-8')).hexdigest().encode('utf-8')
-
- # pseudo-random send/receive pattern
- sender = sockets[nr_send % 2]
- receiver = sockets[1 - (nr_send % 3) % 2]
-
- try:
- sender.sendto(send_data, socket_to_addr[receiver])
- 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:
- break
- except OSError as e:
- if e.errno in [errno.ENOBUFS, errno.ECONNRESET, errno.EPIPE]:
- break
- raise
-
- # Receive as much as we can without blocking
- print("receiving...", nr_send, nr_recv)
- while nr_recv < nr_send:
- for fileno, eventmask in ep.poll():
- receiver = fileno_to_socket[fileno]
-
- if eventmask & select.EPOLLRDNORM:
- while True:
- try:
- recv_data, address = receiver.recvfrom(1024)
- sender = addr_to_socket[address]
- recv_hashes.setdefault((sender.fileno(),
- receiver.fileno()), hashlib.sha256()).update(
- f'<{recv_data}>'.encode('utf-8'))
- nr_recv = nr_recv + 1
- except BlockingIOError as e:
- break
-
- # exercise net/rds/tcp.c:rds_tcp_sysctl_reset()
- for net in [net0, net1]:
- 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)
-
-# the Python socket module doesn't know these
-RDS_INFO_FIRST = 10000
-RDS_INFO_LAST = 10017
-
-nr_success = 0
-nr_error = 0
-
-for s in sockets:
- for optname in range(RDS_INFO_FIRST, RDS_INFO_LAST + 1):
- # Sigh, the Python socket module doesn't allow us to pass
- # buffer lengths greater than 1024 for some reason. RDS
- # wants multiple pages.
- try:
- s.getsockopt(socket.SOL_RDS, optname, 1024)
- nr_success = nr_success + 1
- except OSError as e:
- nr_error = nr_error + 1
- if e.errno == errno.ENOSPC:
- # ignore
- pass
-
-print(f"getsockopt(): {nr_success}/{nr_error}")
+ret = rds_basic.run_test(env)
print("Stopping network packet captures")
subprocess.check_call(['killall', '-q', 'tcpdump'])
-# We're done sending and receiving stuff, now let's check if what
-# we received is what we sent.
-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)
-
- 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)
-
- print(f"{sender}/{receiver}: ok")
-
-print("Success")
-sys.exit(0)
+sys.exit(ret)
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH net-next v1 2/2] selftests: rds: Add rds_stress.py
2026-03-02 5:55 [PATCH net-next v1 0/2] selftests: rds: refactor and expand rds selftests test Allison Henderson
2026-03-02 5:55 ` [PATCH net-next v1 1/2] selftests: rds: Refactor test.py Allison Henderson
@ 2026-03-02 5:55 ` Allison Henderson
2026-03-03 1:33 ` [PATCH net-next v1 0/2] selftests: rds: refactor and expand rds selftests test Jakub Kicinski
2 siblings, 0 replies; 8+ messages in thread
From: Allison Henderson @ 2026-03-02 5:55 UTC (permalink / raw)
To: netdev
Cc: linux-kselftest, pabeni, edumazet, rds-devel, kuba, horms,
linux-rdma, allison.henderson
From: Allison Henderson <allison.henderson@oracle.com>
This patch adds a new rds stress test to the rds selftests suite.
rds_stress is available through the rds-tools package, and can be
run in the selftests infrastructure if it is installed on the host.
We also add new test flags --rds_stress and --rds_basic to the
calling test.py script to run one or both of the tests.
Signed-off-by: Allison Henderson <achender@kernel.org>
---
tools/testing/selftests/net/rds/Makefile | 1 +
tools/testing/selftests/net/rds/rds_stress.py | 58 +++++++++++++++++++
tools/testing/selftests/net/rds/run.sh | 42 ++++++++++++--
tools/testing/selftests/net/rds/test.py | 27 ++++++++-
4 files changed, 123 insertions(+), 5 deletions(-)
diff --git a/tools/testing/selftests/net/rds/Makefile b/tools/testing/selftests/net/rds/Makefile
index 611ed6f2bf91..a37bd4314f0e 100644
--- a/tools/testing/selftests/net/rds/Makefile
+++ b/tools/testing/selftests/net/rds/Makefile
@@ -9,6 +9,7 @@ TEST_FILES := \
include.sh \
test.py \
rds_basic.py \
+ rds_stress.py \
# end of TEST_FILES
EXTRA_CLEAN := \
diff --git a/tools/testing/selftests/net/rds/rds_stress.py b/tools/testing/selftests/net/rds/rds_stress.py
new file mode 100644
index 000000000000..8a86fa0b050d
--- /dev/null
+++ b/tools/testing/selftests/net/rds/rds_stress.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+import subprocess
+import time
+
+def run_test(env):
+ """Run RDS stress selftest.
+
+ env is a dictionary provided by test.py and is expected to contain:
+ - 'addrs': list of (ip, port) tuples matching the sockets
+ - 'netns': list of network namespace names (for sysctl exercises)
+ """
+ addrs = env['addrs'] # [('10.0.0.1', 10000), ('10.0.0.2', 20000)]
+ netns_list = env['netns'] # ['net0', 'net1']
+
+ a0, a1 = addrs
+ recv_addr = a0[0]
+ send_addr = a1[0]
+ port = a0[1]
+
+ nr_tasks = 1 # max child tasks created
+ q_depth = 1 # max outstanding messages
+ duration = 60 # duration of test in seconds
+
+ # server side
+ p0 = subprocess.Popen([
+ 'ip', 'netns', 'exec', netns_list[0],
+ 'rds-stress',
+ '-r', str(recv_addr),
+ '-p', str(port),
+ '-t', str(nr_tasks),
+ '-d', str(q_depth),
+ '-T', str(duration+5) # add some extra time to let the client finish
+ ])
+
+ time.sleep(1) # delay to allow server time to come up
+
+ # client side
+ p1 = subprocess.Popen([
+ 'ip', 'netns', 'exec', netns_list[1],
+ 'rds-stress',
+ '-r', str(send_addr), '-s', str(recv_addr),
+ '-p', str(port),
+ '-t', str(nr_tasks),
+ '-d', str(q_depth),
+ '-T', str(duration)
+ ])
+
+ rc1 = p1.wait() # wait for client
+ rc0 = p0.wait() # then wait for the server
+
+ if rc0 != 0 or rc1 != 0:
+ print(f"rds-stress failed: server={rc0} client={rc1}")
+ return 1
+
+ print("Success")
+ return 0
diff --git a/tools/testing/selftests/net/rds/run.sh b/tools/testing/selftests/net/rds/run.sh
index 8aee244f582a..5917c3222237 100755
--- a/tools/testing/selftests/net/rds/run.sh
+++ b/tools/testing/selftests/net/rds/run.sh
@@ -152,7 +152,34 @@ PLOSS=0
PCORRUPT=0
PDUP=0
GENERATE_GCOV_REPORT=1
-while getopts "d:l:c:u:" opt; do
+RDS_BASIC=0
+RDS_STRESS=0
+FLAGS=""
+
+check_flags()
+{
+ if [ "$RDS_STRESS" -ne 0 ] && ! which rds-stress > /dev/null 2>&1; then
+ echo "selftests: Could not run rds-stress. Disabling rds-stress."
+ RDS_STRESS=0
+ fi
+ if [ "$RDS_STRESS" -eq 0 ] && [ "$RDS_BASIC" -eq 0 ]; then
+ echo "selftests: Default to rds basic tests"
+ RDS_BASIC=1
+ fi
+}
+
+set_flags()
+{
+ if [ "$RDS_STRESS" -ne 0 ];then
+ FLAGS="$FLAGS -s"
+ fi
+
+ if [ "$RDS_BASIC" -ne 0 ]; then
+ FLAGS="$FLAGS -b"
+ fi
+}
+
+while getopts "d:l:c:u:bs" opt; do
case ${opt} in
d)
LOG_DIR=${OPTARG}
@@ -166,9 +193,15 @@ while getopts "d:l:c:u:" opt; do
u)
PDUP=${OPTARG}
;;
+ b)
+ RDS_BASIC=1
+ ;;
+ s)
+ RDS_STRESS=1
+ ;;
:)
echo "USAGE: run.sh [-d logdir] [-l packet_loss] [-c packet_corruption]" \
- "[-u packet_duplcate] [-g]"
+ "[-u packet_duplcate] [-g] [-b] [-s]"
exit 1
;;
?)
@@ -182,7 +215,8 @@ done
check_env
check_conf
check_gcov_conf
-
+check_flags
+set_flags
rm -fr "$LOG_DIR"
TRACE_FILE="${LOG_DIR}/rds-strace.txt"
@@ -195,7 +229,7 @@ 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 400 -d "$LOG_DIR" \
- -l "$PLOSS" -c "$PCORRUPT" -u "$PDUP"
+ -l "$PLOSS" -c "$PCORRUPT" -u "$PDUP" $FLAGS
test_rc=$?
dmesg > "${LOG_DIR}/dmesg.out"
diff --git a/tools/testing/selftests/net/rds/test.py b/tools/testing/selftests/net/rds/test.py
index 0cb060073f6d..6a02809e27e6 100755
--- a/tools/testing/selftests/net/rds/test.py
+++ b/tools/testing/selftests/net/rds/test.py
@@ -9,6 +9,7 @@ import sys
from pwd import getpwuid
from os import stat
import rds_basic
+import rds_stress
# Allow utils module to be imported from different directory
this_dir = os.path.dirname(os.path.realpath(__file__))
@@ -21,6 +22,20 @@ net1 = 'net1'
veth0 = 'veth0'
veth1 = 'veth1'
+def increment_ports(addrs, inc):
+ """Increment port numbers in the addrs list by inc.
+ Use between tests to make the port numbers unique
+
+ addrs: list of (ip, port) tuples
+ inc: int
+ """
+ new_addrs = []
+
+ for addr, port in addrs:
+ new_addrs.append((addr, port + inc))
+
+ return new_addrs
+
def signal_handler(sig, frame):
print('Test timed out')
sys.exit(1)
@@ -31,6 +46,10 @@ 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("-b", "--rds_basic", action="store_true",
+ help="Run rds basic tests")
+parser.add_argument("-s", "--rds_stress", action="store_true",
+ help="Run rds stress tests")
parser.add_argument('--timeout', help="timeout to terminate hung test",
type=int, default=0)
parser.add_argument('-l', '--loss', help="Simulate tcp packet loss",
@@ -102,7 +121,13 @@ env = {
'netns': [net0, net1],
}
-ret = rds_basic.run_test(env)
+ret = 0
+if args.rds_basic:
+ ret = rds_basic.run_test(env)
+
+if ret == 0 and args.rds_stress:
+ env['addrs'] = increment_ports(env['addrs'], 1000)
+ ret = rds_stress.run_test(env)
print("Stopping network packet captures")
subprocess.check_call(['killall', '-q', 'tcpdump'])
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread