Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests: rds: add testing infrastructure

This adds some basic self-testing infrastructure for RDS-TCP.

Signed-off-by: Vegard Nossum <vegard.nossum@oracle.com>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
Signed-off-by: Allison Henderson <allison.henderson@oracle.com>
Signed-off-by: David S. Miller <davem@davemloft.net>

authored by

Vegard Nossum and committed by
David S. Miller
3ade6ce1 bc75dcc3

+605
+11
Documentation/dev-tools/gcov.rst
··· 75 75 kernel modules are supported by this mechanism. 76 76 77 77 78 + Module specific configs 79 + ----------------------- 80 + 81 + Gcov kernel configs for specific modules are described below: 82 + 83 + CONFIG_GCOV_PROFILE_RDS: 84 + Enables GCOV profiling on RDS for checking which functions or 85 + lines are executed. This config is used by the rds selftest to 86 + generate coverage reports. If left unset the report is omitted. 87 + 88 + 78 89 Files 79 90 ----- 80 91
+1
MAINTAINERS
··· 19197 19197 W: https://oss.oracle.com/projects/rds/ 19198 19198 F: Documentation/networking/rds.rst 19199 19199 F: net/rds/ 19200 + F: tools/testing/selftests/net/rds/ 19200 19201 19201 19202 RDT - RESOURCE ALLOCATION 19202 19203 M: Fenghua Yu <fenghua.yu@intel.com>
+1
tools/testing/selftests/Makefile
··· 68 68 TARGETS += net/openvswitch 69 69 TARGETS += net/tcp_ao 70 70 TARGETS += net/netfilter 71 + TARGETS += net/rds 71 72 TARGETS += nsfs 72 73 TARGETS += perf_events 73 74 TARGETS += pidfd
+12
tools/testing/selftests/net/rds/Makefile
··· 1 + # SPDX-License-Identifier: GPL-2.0 2 + 3 + all: 4 + @echo mk_build_dir="$(shell pwd)" > include.sh 5 + 6 + TEST_PROGS := run.sh \ 7 + include.sh \ 8 + test.py 9 + 10 + EXTRA_CLEAN := /tmp/rds_logs 11 + 12 + include ../../lib.mk
+41
tools/testing/selftests/net/rds/README.txt
··· 1 + RDS self-tests 2 + ============== 3 + 4 + These scripts provide a coverage test for RDS-TCP by creating two 5 + network namespaces and running rds packets between them. A loopback 6 + network is provisioned with optional probability of packet loss or 7 + corruption. A workload of 50000 hashes, each 64 characters in size, 8 + are passed over an RDS socket on this test network. A passing test means 9 + the RDS-TCP stack was able to recover properly. The provided config.sh 10 + can be used to compile the kernel with the necessary gcov options. The 11 + kernel may optionally be configured to omit the coverage report as well. 12 + 13 + USAGE: 14 + run.sh [-d logdir] [-l packet_loss] [-c packet_corruption] 15 + [-u packet_duplcate] 16 + 17 + OPTIONS: 18 + -d Log directory. Defaults to tools/testing/selftests/net/rds/rds_logs 19 + 20 + -l Simulates a percentage of packet loss 21 + 22 + -c Simulates a percentage of packet corruption 23 + 24 + -u Simulates a percentage of packet duplication. 25 + 26 + EXAMPLE: 27 + 28 + # Create a suitable gcov enabled .config 29 + tools/testing/selftests/net/rds/config.sh -g 30 + 31 + # Alternatly create a gcov disabled .config 32 + tools/testing/selftests/net/rds/config.sh 33 + 34 + # build the kernel 35 + vng --build --config tools/testing/selftests/net/config 36 + 37 + # launch the tests in a VM 38 + vng -v --rwdir ./ --run . --user root --cpus 4 -- \ 39 + "export PYTHONPATH=tools/testing/selftests/net/; tools/testing/selftests/net/rds/run.sh" 40 + 41 + An HTML coverage report will be output in tools/testing/selftests/net/rds/rds_logs/coverage/.
+53
tools/testing/selftests/net/rds/config.sh
··· 1 + #! /bin/bash 2 + # SPDX-License-Identifier: GPL-2.0 3 + 4 + set -e 5 + set -u 6 + set -x 7 + 8 + unset KBUILD_OUTPUT 9 + 10 + GENERATE_GCOV_REPORT=0 11 + while getopts "g" opt; do 12 + case ${opt} in 13 + g) 14 + GENERATE_GCOV_REPORT=1 15 + ;; 16 + :) 17 + echo "USAGE: config.sh [-g]" 18 + exit 1 19 + ;; 20 + ?) 21 + echo "Invalid option: -${OPTARG}." 22 + exit 1 23 + ;; 24 + esac 25 + done 26 + 27 + CONF_FILE="tools/testing/selftests/net/config" 28 + 29 + # no modules 30 + scripts/config --file "$CONF_FILE" --disable CONFIG_MODULES 31 + 32 + # enable RDS 33 + scripts/config --file "$CONF_FILE" --enable CONFIG_RDS 34 + scripts/config --file "$CONF_FILE" --enable CONFIG_RDS_TCP 35 + 36 + if [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then 37 + # instrument RDS and only RDS 38 + scripts/config --file "$CONF_FILE" --enable CONFIG_GCOV_KERNEL 39 + scripts/config --file "$CONF_FILE" --disable GCOV_PROFILE_ALL 40 + scripts/config --file "$CONF_FILE" --enable GCOV_PROFILE_RDS 41 + else 42 + scripts/config --file "$CONF_FILE" --disable CONFIG_GCOV_KERNEL 43 + scripts/config --file "$CONF_FILE" --disable GCOV_PROFILE_ALL 44 + scripts/config --file "$CONF_FILE" --disable GCOV_PROFILE_RDS 45 + fi 46 + 47 + # need network namespaces to run tests with veth network interfaces 48 + scripts/config --file "$CONF_FILE" --enable CONFIG_NET_NS 49 + scripts/config --file "$CONF_FILE" --enable CONFIG_VETH 50 + 51 + # simulate packet loss 52 + scripts/config --file "$CONF_FILE" --enable CONFIG_NET_SCH_NETEM 53 +
+224
tools/testing/selftests/net/rds/run.sh
··· 1 + #! /bin/bash 2 + # SPDX-License-Identifier: GPL-2.0 3 + 4 + set -e 5 + set -u 6 + 7 + unset KBUILD_OUTPUT 8 + 9 + current_dir="$(realpath "$(dirname "$0")")" 10 + build_dir="$current_dir" 11 + 12 + build_include="$current_dir/include.sh" 13 + if test -f "$build_include"; then 14 + # this include will define "$mk_build_dir" as the location the test was 15 + # built. We will need this if the tests are installed in a location 16 + # other than the kernel source 17 + 18 + source "$build_include" 19 + build_dir="$mk_build_dir" 20 + fi 21 + 22 + # This test requires kernel source and the *.gcda data therein 23 + # Locate the top level of the kernel source, and the net/rds 24 + # subfolder with the appropriate *.gcno object files 25 + ksrc_dir="$(realpath "$build_dir"/../../../../../)" 26 + kconfig="$ksrc_dir/.config" 27 + obj_dir="$ksrc_dir/net/rds" 28 + 29 + GCOV_CMD=gcov 30 + 31 + #check to see if the host has the required packages to generate a gcov report 32 + check_gcov_env() 33 + { 34 + if ! which "$GCOV_CMD" > /dev/null 2>&1; then 35 + echo "Warning: Could not find gcov. " 36 + GENERATE_GCOV_REPORT=0 37 + return 38 + fi 39 + 40 + # the gcov version must match the gcc version 41 + GCC_VER=$(gcc -dumpfullversion) 42 + GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| awk 'BEGIN {FS="-"}{print $1}') 43 + if [ "$GCOV_VER" != "$GCC_VER" ]; then 44 + #attempt to find a matching gcov version 45 + GCOV_CMD=gcov-$(gcc -dumpversion) 46 + 47 + if ! which "$GCOV_CMD" > /dev/null 2>&1; then 48 + echo "Warning: Could not find an appropriate gcov installation. \ 49 + gcov version must match gcc version" 50 + GENERATE_GCOV_REPORT=0 51 + return 52 + fi 53 + 54 + #recheck version number of found gcov executable 55 + GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| \ 56 + awk 'BEGIN {FS="-"}{print $1}') 57 + if [ "$GCOV_VER" != "$GCC_VER" ]; then 58 + echo "Warning: Could not find an appropriate gcov installation. \ 59 + gcov version must match gcc version" 60 + GENERATE_GCOV_REPORT=0 61 + else 62 + echo "Warning: Mismatched gcc and gcov detected. Using $GCOV_CMD" 63 + fi 64 + fi 65 + } 66 + 67 + # Check to see if the kconfig has the required configs to generate a coverage report 68 + check_gcov_conf() 69 + { 70 + if ! grep -x "CONFIG_GCOV_PROFILE_RDS=y" "$kconfig" > /dev/null 2>&1; then 71 + echo "INFO: CONFIG_GCOV_PROFILE_RDS should be enabled for coverage reports" 72 + GENERATE_GCOV_REPORT=0 73 + fi 74 + if ! grep -x "CONFIG_GCOV_KERNEL=y" "$kconfig" > /dev/null 2>&1; then 75 + echo "INFO: CONFIG_GCOV_KERNEL should be enabled for coverage reports" 76 + GENERATE_GCOV_REPORT=0 77 + fi 78 + if grep -x "CONFIG_GCOV_PROFILE_ALL=y" "$kconfig" > /dev/null 2>&1; then 79 + echo "INFO: CONFIG_GCOV_PROFILE_ALL should be disabled for coverage reports" 80 + GENERATE_GCOV_REPORT=0 81 + fi 82 + 83 + if [ "$GENERATE_GCOV_REPORT" -eq 0 ]; then 84 + echo "To enable gcov reports, please run "\ 85 + "\"tools/testing/selftests/net/rds/config.sh -g\" and rebuild the kernel" 86 + else 87 + # if we have the required kernel configs, proceed to check the environment to 88 + # ensure we have the required gcov packages 89 + check_gcov_env 90 + fi 91 + } 92 + 93 + # Kselftest framework requirement - SKIP code is 4. 94 + check_conf_enabled() { 95 + if ! grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then 96 + echo "selftests: [SKIP] This test requires $1 enabled" 97 + echo "Please run tools/testing/selftests/net/rds/config.sh and rebuild the kernel" 98 + exit 4 99 + fi 100 + } 101 + check_conf_disabled() { 102 + if grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then 103 + echo "selftests: [SKIP] This test requires $1 disabled" 104 + echo "Please run tools/testing/selftests/net/rds/config.sh and rebuild the kernel" 105 + exit 4 106 + fi 107 + } 108 + check_conf() { 109 + check_conf_enabled CONFIG_NET_SCH_NETEM 110 + check_conf_enabled CONFIG_VETH 111 + check_conf_enabled CONFIG_NET_NS 112 + check_conf_enabled CONFIG_RDS_TCP 113 + check_conf_enabled CONFIG_RDS 114 + check_conf_disabled CONFIG_MODULES 115 + } 116 + 117 + check_env() 118 + { 119 + if ! test -d "$obj_dir"; then 120 + echo "selftests: [SKIP] This test requires a kernel source tree" 121 + exit 4 122 + fi 123 + if ! test -e "$kconfig"; then 124 + echo "selftests: [SKIP] This test requires a configured kernel source tree" 125 + exit 4 126 + fi 127 + if ! which strace > /dev/null 2>&1; then 128 + echo "selftests: [SKIP] Could not run test without strace" 129 + exit 4 130 + fi 131 + if ! which tcpdump > /dev/null 2>&1; then 132 + echo "selftests: [SKIP] Could not run test without tcpdump" 133 + exit 4 134 + fi 135 + 136 + if ! which python3 > /dev/null 2>&1; then 137 + echo "selftests: [SKIP] Could not run test without python3" 138 + exit 4 139 + fi 140 + 141 + python_major=$(python3 -c "import sys; print(sys.version_info[0])") 142 + python_minor=$(python3 -c "import sys; print(sys.version_info[1])") 143 + if [[ python_major -lt 3 || ( python_major -eq 3 && python_minor -lt 9 ) ]] ; then 144 + echo "selftests: [SKIP] Could not run test without at least python3.9" 145 + python3 -V 146 + exit 4 147 + fi 148 + } 149 + 150 + LOG_DIR="$current_dir"/rds_logs 151 + PLOSS=0 152 + PCORRUPT=0 153 + PDUP=0 154 + GENERATE_GCOV_REPORT=1 155 + while getopts "d:l:c:u:" opt; do 156 + case ${opt} in 157 + d) 158 + LOG_DIR=${OPTARG} 159 + ;; 160 + l) 161 + PLOSS=${OPTARG} 162 + ;; 163 + c) 164 + PCORRUPT=${OPTARG} 165 + ;; 166 + u) 167 + PDUP=${OPTARG} 168 + ;; 169 + :) 170 + echo "USAGE: run.sh [-d logdir] [-l packet_loss] [-c packet_corruption]" \ 171 + "[-u packet_duplcate] [-g]" 172 + exit 1 173 + ;; 174 + ?) 175 + echo "Invalid option: -${OPTARG}." 176 + exit 1 177 + ;; 178 + esac 179 + done 180 + 181 + 182 + check_env 183 + check_conf 184 + check_gcov_conf 185 + 186 + 187 + rm -fr "$LOG_DIR" 188 + TRACE_FILE="${LOG_DIR}/rds-strace.txt" 189 + COVR_DIR="${LOG_DIR}/coverage/" 190 + mkdir -p "$LOG_DIR" 191 + mkdir -p "$COVR_DIR" 192 + 193 + set +e 194 + echo running RDS tests... 195 + echo Traces will be logged to "$TRACE_FILE" 196 + rm -f "$TRACE_FILE" 197 + strace -T -tt -o "$TRACE_FILE" python3 "$(dirname "$0")/test.py" --timeout 400 -d "$LOG_DIR" \ 198 + -l "$PLOSS" -c "$PCORRUPT" -u "$PDUP" 199 + 200 + test_rc=$? 201 + dmesg > "${LOG_DIR}/dmesg.out" 202 + 203 + if [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then 204 + echo saving coverage data... 205 + (set +x; cd /sys/kernel/debug/gcov; find ./* -name '*.gcda' | \ 206 + while read -r f 207 + do 208 + cat < "/sys/kernel/debug/gcov/$f" > "/$f" 209 + done) 210 + 211 + echo running gcovr... 212 + gcovr -s --html-details --gcov-executable "$GCOV_CMD" --gcov-ignore-parse-errors \ 213 + -o "${COVR_DIR}/gcovr" "${ksrc_dir}/net/rds/" 214 + else 215 + echo "Coverage report will be skipped" 216 + fi 217 + 218 + if [ "$test_rc" -eq 0 ]; then 219 + echo "PASS: Test completed successfully" 220 + else 221 + echo "FAIL: Test failed" 222 + fi 223 + 224 + exit "$test_rc"
+262
tools/testing/selftests/net/rds/test.py
··· 1 + #! /usr/bin/env python3 2 + # SPDX-License-Identifier: GPL-2.0 3 + 4 + import argparse 5 + import ctypes 6 + import errno 7 + import hashlib 8 + import os 9 + import select 10 + import signal 11 + import socket 12 + import subprocess 13 + import sys 14 + import atexit 15 + from pwd import getpwuid 16 + from os import stat 17 + from lib.py import ip 18 + 19 + 20 + libc = ctypes.cdll.LoadLibrary('libc.so.6') 21 + setns = libc.setns 22 + 23 + net0 = 'net0' 24 + net1 = 'net1' 25 + 26 + veth0 = 'veth0' 27 + veth1 = 'veth1' 28 + 29 + # Helper function for creating a socket inside a network namespace. 30 + # We need this because otherwise RDS will detect that the two TCP 31 + # sockets are on the same interface and use the loop transport instead 32 + # of the TCP transport. 33 + def netns_socket(netns, *args): 34 + u0, u1 = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET) 35 + 36 + child = os.fork() 37 + if child == 0: 38 + # change network namespace 39 + with open(f'/var/run/netns/{netns}') as f: 40 + try: 41 + ret = setns(f.fileno(), 0) 42 + except IOError as e: 43 + print(e.errno) 44 + print(e) 45 + 46 + # create socket in target namespace 47 + s = socket.socket(*args) 48 + 49 + # send resulting socket to parent 50 + socket.send_fds(u0, [], [s.fileno()]) 51 + 52 + sys.exit(0) 53 + 54 + # receive socket from child 55 + _, s, _, _ = socket.recv_fds(u1, 0, 1) 56 + os.waitpid(child, 0) 57 + u0.close() 58 + u1.close() 59 + return socket.fromfd(s[0], *args) 60 + 61 + def signal_handler(sig, frame): 62 + print('Test timed out') 63 + sys.exit(1) 64 + 65 + #Parse out command line arguments. We take an optional 66 + # timeout parameter and an optional log output folder 67 + parser = argparse.ArgumentParser(description="init script args", 68 + formatter_class=argparse.ArgumentDefaultsHelpFormatter) 69 + parser.add_argument("-d", "--logdir", action="store", 70 + help="directory to store logs", default="/tmp") 71 + parser.add_argument('--timeout', help="timeout to terminate hung test", 72 + type=int, default=0) 73 + parser.add_argument('-l', '--loss', help="Simulate tcp packet loss", 74 + type=int, default=0) 75 + parser.add_argument('-c', '--corruption', help="Simulate tcp packet corruption", 76 + type=int, default=0) 77 + parser.add_argument('-u', '--duplicate', help="Simulate tcp packet duplication", 78 + type=int, default=0) 79 + args = parser.parse_args() 80 + logdir=args.logdir 81 + packet_loss=str(args.loss)+'%' 82 + packet_corruption=str(args.corruption)+'%' 83 + packet_duplicate=str(args.duplicate)+'%' 84 + 85 + ip(f"netns add {net0}") 86 + ip(f"netns add {net1}") 87 + ip(f"link add type veth") 88 + 89 + addrs = [ 90 + # we technically don't need different port numbers, but this will 91 + # help identify traffic in the network analyzer 92 + ('10.0.0.1', 10000), 93 + ('10.0.0.2', 20000), 94 + ] 95 + 96 + # move interfaces to separate namespaces so they can no longer be 97 + # bound directly; this prevents rds from switching over from the tcp 98 + # transport to the loop transport. 99 + ip(f"link set {veth0} netns {net0} up") 100 + ip(f"link set {veth1} netns {net1} up") 101 + 102 + 103 + 104 + # add addresses 105 + ip(f"-n {net0} addr add {addrs[0][0]}/32 dev {veth0}") 106 + ip(f"-n {net1} addr add {addrs[1][0]}/32 dev {veth1}") 107 + 108 + # add routes 109 + ip(f"-n {net0} route add {addrs[1][0]}/32 dev {veth0}") 110 + ip(f"-n {net1} route add {addrs[0][0]}/32 dev {veth1}") 111 + 112 + # sanity check that our two interfaces/addresses are correctly set up 113 + # and communicating by doing a single ping 114 + ip(f"netns exec {net0} ping -c 1 {addrs[1][0]}") 115 + 116 + # Start a packet capture on each network 117 + for net in [net0, net1]: 118 + tcpdump_pid = os.fork() 119 + if tcpdump_pid == 0: 120 + pcap = logdir+'/'+net+'.pcap' 121 + subprocess.check_call(['touch', pcap]) 122 + user = getpwuid(stat(pcap).st_uid).pw_name 123 + ip(f"netns exec {net} /usr/sbin/tcpdump -Z {user} -i any -w {pcap}") 124 + sys.exit(0) 125 + 126 + # simulate packet loss, duplication and corruption 127 + for net, iface in [(net0, veth0), (net1, veth1)]: 128 + ip(f"netns exec {net} /usr/sbin/tc qdisc add dev {iface} root netem \ 129 + corrupt {packet_corruption} loss {packet_loss} duplicate \ 130 + {packet_duplicate}") 131 + 132 + # add a timeout 133 + if args.timeout > 0: 134 + signal.alarm(args.timeout) 135 + signal.signal(signal.SIGALRM, signal_handler) 136 + 137 + sockets = [ 138 + netns_socket(net0, socket.AF_RDS, socket.SOCK_SEQPACKET), 139 + netns_socket(net1, socket.AF_RDS, socket.SOCK_SEQPACKET), 140 + ] 141 + 142 + for s, addr in zip(sockets, addrs): 143 + s.bind(addr) 144 + s.setblocking(0) 145 + 146 + fileno_to_socket = { 147 + s.fileno(): s for s in sockets 148 + } 149 + 150 + addr_to_socket = { 151 + addr: s for addr, s in zip(addrs, sockets) 152 + } 153 + 154 + socket_to_addr = { 155 + s: addr for addr, s in zip(addrs, sockets) 156 + } 157 + 158 + send_hashes = {} 159 + recv_hashes = {} 160 + 161 + ep = select.epoll() 162 + 163 + for s in sockets: 164 + ep.register(s, select.EPOLLRDNORM) 165 + 166 + n = 50000 167 + nr_send = 0 168 + nr_recv = 0 169 + 170 + while nr_send < n: 171 + # Send as much as we can without blocking 172 + print("sending...", nr_send, nr_recv) 173 + while nr_send < n: 174 + send_data = hashlib.sha256( 175 + f'packet {nr_send}'.encode('utf-8')).hexdigest().encode('utf-8') 176 + 177 + # pseudo-random send/receive pattern 178 + sender = sockets[nr_send % 2] 179 + receiver = sockets[1 - (nr_send % 3) % 2] 180 + 181 + try: 182 + sender.sendto(send_data, socket_to_addr[receiver]) 183 + send_hashes.setdefault((sender.fileno(), receiver.fileno()), 184 + hashlib.sha256()).update(f'<{send_data}>'.encode('utf-8')) 185 + nr_send = nr_send + 1 186 + except BlockingIOError as e: 187 + break 188 + except OSError as e: 189 + if e.errno in [errno.ENOBUFS, errno.ECONNRESET, errno.EPIPE]: 190 + break 191 + raise 192 + 193 + # Receive as much as we can without blocking 194 + print("receiving...", nr_send, nr_recv) 195 + while nr_recv < nr_send: 196 + for fileno, eventmask in ep.poll(): 197 + receiver = fileno_to_socket[fileno] 198 + 199 + if eventmask & select.EPOLLRDNORM: 200 + while True: 201 + try: 202 + recv_data, address = receiver.recvfrom(1024) 203 + sender = addr_to_socket[address] 204 + recv_hashes.setdefault((sender.fileno(), 205 + receiver.fileno()), hashlib.sha256()).update( 206 + f'<{recv_data}>'.encode('utf-8')) 207 + nr_recv = nr_recv + 1 208 + except BlockingIOError as e: 209 + break 210 + 211 + # exercise net/rds/tcp.c:rds_tcp_sysctl_reset() 212 + for net in [net0, net1]: 213 + ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_rcvbuf=10000") 214 + ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_sndbuf=10000") 215 + 216 + print("done", nr_send, nr_recv) 217 + 218 + # the Python socket module doesn't know these 219 + RDS_INFO_FIRST = 10000 220 + RDS_INFO_LAST = 10017 221 + 222 + nr_success = 0 223 + nr_error = 0 224 + 225 + for s in sockets: 226 + for optname in range(RDS_INFO_FIRST, RDS_INFO_LAST + 1): 227 + # Sigh, the Python socket module doesn't allow us to pass 228 + # buffer lengths greater than 1024 for some reason. RDS 229 + # wants multiple pages. 230 + try: 231 + s.getsockopt(socket.SOL_RDS, optname, 1024) 232 + nr_success = nr_success + 1 233 + except OSError as e: 234 + nr_error = nr_error + 1 235 + if e.errno == errno.ENOSPC: 236 + # ignore 237 + pass 238 + 239 + print(f"getsockopt(): {nr_success}/{nr_error}") 240 + 241 + print("Stopping network packet captures") 242 + subprocess.check_call(['killall', '-q', 'tcpdump']) 243 + 244 + # We're done sending and receiving stuff, now let's check if what 245 + # we received is what we sent. 246 + for (sender, receiver), send_hash in send_hashes.items(): 247 + recv_hash = recv_hashes.get((sender, receiver)) 248 + 249 + if recv_hash is None: 250 + print("FAIL: No data received") 251 + sys.exit(1) 252 + 253 + if send_hash.hexdigest() != recv_hash.hexdigest(): 254 + print("FAIL: Send/recv mismatch") 255 + print("hash expected:", send_hash.hexdigest()) 256 + print("hash received:", recv_hash.hexdigest()) 257 + sys.exit(1) 258 + 259 + print(f"{sender}/{receiver}: ok") 260 + 261 + print("Success") 262 + sys.exit(0)