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

Merge branch 'selftests-add-selftest-for-link-layer-and-performance-testing'

Mohan Prasad says:

====================
selftests: Add selftest for link layer and performance testing

The series of patches are for doing basic tests
of NIC driver. Test comprises checks for auto-negotiation,
speed, duplex state and throughput between local NIC and
partner. Tools such as ethtool, iperf3 are used.

Signed-off-by: Mohan Prasad J <mohan.prasad@microchip.com>
====================

Link: https://patch.msgid.link/20241114192545.1742514-1-mohan.prasad@microchip.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>

+494 -1
+2
tools/testing/selftests/drivers/net/hw/Makefile
··· 11 11 hw_stats_l3.sh \ 12 12 hw_stats_l3_gre.sh \ 13 13 loopback.sh \ 14 + nic_link_layer.py \ 15 + nic_performance.py \ 14 16 pp_alloc_fail.py \ 15 17 rss_ctx.py \ 16 18 #
+1
tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
··· 9 9 sys.path.append(KSFT_DIR.as_posix()) 10 10 from net.lib.py import * 11 11 from drivers.net.lib.py import * 12 + from .linkconfig import LinkConfig 12 13 except ModuleNotFoundError as e: 13 14 ksft_pr("Failed importing `net` library from kernel sources") 14 15 ksft_pr(str(e))
+222
tools/testing/selftests/drivers/net/hw/lib/py/linkconfig.py
··· 1 + # SPDX-License-Identifier: GPL-2.0 2 + 3 + from lib.py import cmd, ethtool, ip 4 + from lib.py import ksft_pr, ksft_eq, KsftSkipEx 5 + from typing import Optional 6 + import re 7 + import time 8 + import json 9 + 10 + #The LinkConfig class is implemented to handle the link layer configurations. 11 + #Required minimum ethtool version is 6.10 12 + 13 + class LinkConfig: 14 + """Class for handling the link layer configurations""" 15 + def __init__(self, cfg: object) -> None: 16 + self.cfg = cfg 17 + self.partner_netif = self.get_partner_netif_name() 18 + 19 + """Get the initial link configuration of local interface""" 20 + self.common_link_modes = self.get_common_link_modes() 21 + 22 + def get_partner_netif_name(self) -> Optional[str]: 23 + partner_netif = None 24 + try: 25 + if not self.verify_link_up(): 26 + return None 27 + """Get partner interface name""" 28 + partner_json_output = ip("addr show", json=True, host=self.cfg.remote) 29 + for interface in partner_json_output: 30 + for addr in interface.get('addr_info', []): 31 + if addr.get('local') == self.cfg.remote_addr: 32 + partner_netif = interface['ifname'] 33 + ksft_pr(f"Partner Interface name: {partner_netif}") 34 + if partner_netif is None: 35 + ksft_pr("Unable to get the partner interface name") 36 + except Exception as e: 37 + print(f"Unexpected error occurred while getting partner interface name: {e}") 38 + self.partner_netif = partner_netif 39 + return partner_netif 40 + 41 + def verify_link_up(self) -> bool: 42 + """Verify whether the local interface link is up""" 43 + with open(f"/sys/class/net/{self.cfg.ifname}/operstate", "r") as fp: 44 + link_state = fp.read().strip() 45 + 46 + if link_state == "down": 47 + ksft_pr(f"Link state of interface {self.cfg.ifname} is DOWN") 48 + return False 49 + else: 50 + return True 51 + 52 + def reset_interface(self, local: bool = True, remote: bool = True) -> bool: 53 + ksft_pr("Resetting interfaces in local and remote") 54 + if remote: 55 + if self.verify_link_up(): 56 + if self.partner_netif is not None: 57 + ifname = self.partner_netif 58 + link_up_cmd = f"ip link set up {ifname}" 59 + link_down_cmd = f"ip link set down {ifname}" 60 + reset_cmd = f"{link_down_cmd} && sleep 5 && {link_up_cmd}" 61 + try: 62 + cmd(reset_cmd, host=self.cfg.remote) 63 + except Exception as e: 64 + ksft_pr(f"Unexpected error occurred while resetting remote: {e}") 65 + else: 66 + ksft_pr("Partner interface not available") 67 + if local: 68 + ifname = self.cfg.ifname 69 + link_up_cmd = f"ip link set up {ifname}" 70 + link_down_cmd = f"ip link set down {ifname}" 71 + reset_cmd = f"{link_down_cmd} && sleep 5 && {link_up_cmd}" 72 + try: 73 + cmd(reset_cmd) 74 + except Exception as e: 75 + ksft_pr(f"Unexpected error occurred while resetting local: {e}") 76 + time.sleep(10) 77 + if self.verify_link_up() and self.get_ethtool_field("link-detected"): 78 + ksft_pr("Local and remote interfaces reset to original state") 79 + return True 80 + else: 81 + ksft_pr("Error occurred after resetting interfaces. Link is DOWN.") 82 + return False 83 + 84 + def set_speed_and_duplex(self, speed: str, duplex: str, autoneg: bool = True) -> bool: 85 + """Set the speed and duplex state for the interface""" 86 + autoneg_state = "on" if autoneg is True else "off" 87 + process = None 88 + try: 89 + process = ethtool(f"--change {self.cfg.ifname} speed {speed} duplex {duplex} autoneg {autoneg_state}") 90 + except Exception as e: 91 + ksft_pr(f"Unexpected error occurred while setting speed/duplex: {e}") 92 + if process is None or process.ret != 0: 93 + return False 94 + else: 95 + ksft_pr(f"Speed: {speed} Mbps, Duplex: {duplex} set for Interface: {self.cfg.ifname}") 96 + return True 97 + 98 + def verify_speed_and_duplex(self, expected_speed: str, expected_duplex: str) -> bool: 99 + if not self.verify_link_up(): 100 + return False 101 + """Verifying the speed and duplex state for the interface""" 102 + with open(f"/sys/class/net/{self.cfg.ifname}/speed", "r") as fp: 103 + actual_speed = fp.read().strip() 104 + with open(f"/sys/class/net/{self.cfg.ifname}/duplex", "r") as fp: 105 + actual_duplex = fp.read().strip() 106 + 107 + ksft_eq(actual_speed, expected_speed) 108 + ksft_eq(actual_duplex, expected_duplex) 109 + return True 110 + 111 + def set_autonegotiation_state(self, state: str, remote: bool = False) -> bool: 112 + common_link_modes = self.common_link_modes 113 + speeds, duplex_modes = self.get_speed_duplex_values(self.common_link_modes) 114 + speed = speeds[0] 115 + duplex = duplex_modes[0] 116 + if not speed or not duplex: 117 + ksft_pr("No speed or duplex modes found") 118 + return False 119 + 120 + speed_duplex_cmd = f"speed {speed} duplex {duplex}" if state == "off" else "" 121 + if remote: 122 + if not self.verify_link_up(): 123 + return False 124 + """Set the autonegotiation state for the partner""" 125 + command = f"-s {self.partner_netif} {speed_duplex_cmd} autoneg {state}" 126 + partner_autoneg_change = None 127 + """Set autonegotiation state for interface in remote pc""" 128 + try: 129 + partner_autoneg_change = ethtool(command, host=self.cfg.remote) 130 + except Exception as e: 131 + ksft_pr(f"Unexpected error occurred while changing auto-neg in remote: {e}") 132 + if partner_autoneg_change is None or partner_autoneg_change.ret != 0: 133 + ksft_pr(f"Not able to set autoneg parameter for interface {self.partner_netif}.") 134 + return False 135 + ksft_pr(f"Autoneg set as {state} for {self.partner_netif}") 136 + else: 137 + """Set the autonegotiation state for the interface""" 138 + try: 139 + process = ethtool(f"-s {self.cfg.ifname} {speed_duplex_cmd} autoneg {state}") 140 + if process.ret != 0: 141 + ksft_pr(f"Not able to set autoneg parameter for interface {self.cfg.ifname}") 142 + return False 143 + except Exception as e: 144 + ksft_pr(f"Unexpected error occurred while changing auto-neg in local: {e}") 145 + return False 146 + ksft_pr(f"Autoneg set as {state} for {self.cfg.ifname}") 147 + return True 148 + 149 + def check_autoneg_supported(self, remote: bool = False) -> bool: 150 + if not remote: 151 + local_autoneg = self.get_ethtool_field("supports-auto-negotiation") 152 + if local_autoneg is None: 153 + ksft_pr(f"Unable to fetch auto-negotiation status for interface {self.cfg.ifname}") 154 + """Return autoneg status of the local interface""" 155 + return local_autoneg 156 + else: 157 + if not self.verify_link_up(): 158 + raise KsftSkipEx("Link is DOWN") 159 + """Check remote auto-negotiation support status""" 160 + partner_autoneg = False 161 + if self.partner_netif is not None: 162 + partner_autoneg = self.get_ethtool_field("supports-auto-negotiation", remote=True) 163 + if partner_autoneg is None: 164 + ksft_pr(f"Unable to fetch auto-negotiation status for interface {self.partner_netif}") 165 + return partner_autoneg 166 + 167 + def get_common_link_modes(self) -> set[str]: 168 + common_link_modes = [] 169 + """Populate common link modes""" 170 + link_modes = self.get_ethtool_field("supported-link-modes") 171 + partner_link_modes = self.get_ethtool_field("link-partner-advertised-link-modes") 172 + if link_modes is None: 173 + raise KsftSkipEx(f"Link modes not available for {self.cfg.ifname}") 174 + if partner_link_modes is None: 175 + raise KsftSkipEx(f"Partner link modes not available for {self.cfg.ifname}") 176 + common_link_modes = set(link_modes) and set(partner_link_modes) 177 + return common_link_modes 178 + 179 + def get_speed_duplex_values(self, link_modes: list[str]) -> tuple[list[str], list[str]]: 180 + speed = [] 181 + duplex = [] 182 + """Check the link modes""" 183 + for data in link_modes: 184 + parts = data.split('/') 185 + speed_value = re.match(r'\d+', parts[0]) 186 + if speed_value: 187 + speed.append(speed_value.group()) 188 + else: 189 + ksft_pr(f"No speed value found for interface {self.ifname}") 190 + return None, None 191 + duplex.append(parts[1].lower()) 192 + return speed, duplex 193 + 194 + def get_ethtool_field(self, field: str, remote: bool = False) -> Optional[str]: 195 + process = None 196 + if not remote: 197 + """Get the ethtool field value for the local interface""" 198 + try: 199 + process = ethtool(self.cfg.ifname, json=True) 200 + except Exception as e: 201 + ksft_pr("Required minimum ethtool version is 6.10") 202 + ksft_pr(f"Unexpected error occurred while getting ethtool field in local: {e}") 203 + return None 204 + else: 205 + if not self.verify_link_up(): 206 + return None 207 + """Get the ethtool field value for the remote interface""" 208 + self.cfg.require_cmd("ethtool", remote=True) 209 + if self.partner_netif is None: 210 + ksft_pr(f"Partner interface name is unavailable.") 211 + return None 212 + try: 213 + process = ethtool(self.partner_netif, json=True, host=self.cfg.remote) 214 + except Exception as e: 215 + ksft_pr("Required minimum ethtool version is 6.10") 216 + ksft_pr(f"Unexpected error occurred while getting ethtool field in remote: {e}") 217 + return None 218 + json_data = process[0] 219 + """Check if the field exist in the json data""" 220 + if field not in json_data: 221 + raise KsftSkipEx(f"Field {field} does not exist in the output of interface {json_data["ifname"]}") 222 + return json_data[field]
+137
tools/testing/selftests/drivers/net/hw/nic_performance.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: GPL-2.0 3 + 4 + #Introduction: 5 + #This file has basic performance test for generic NIC drivers. 6 + #The test comprises of throughput check for TCP and UDP streams. 7 + # 8 + #Setup: 9 + #Connect the DUT PC with NIC card to partner pc back via ethernet medium of your choice(RJ45, T1) 10 + # 11 + # DUT PC Partner PC 12 + #┌───────────────────────┐ ┌──────────────────────────┐ 13 + #│ │ │ │ 14 + #│ │ │ │ 15 + #│ ┌───────────┐ │ │ 16 + #│ │DUT NIC │ Eth │ │ 17 + #│ │Interface ─┼─────────────────────────┼─ any eth Interface │ 18 + #│ └───────────┘ │ │ 19 + #│ │ │ │ 20 + #│ │ │ │ 21 + #└───────────────────────┘ └──────────────────────────┘ 22 + # 23 + #Configurations: 24 + #To prevent interruptions, Add ethtool, ip to the sudoers list in remote PC and get the ssh key from remote. 25 + #Required minimum ethtool version is 6.10 26 + #Change the below configuration based on your hw needs. 27 + # """Default values""" 28 + #time_delay = 8 #time taken to wait for transitions to happen, in seconds. 29 + #test_duration = 10 #performance test duration for the throughput check, in seconds. 30 + #send_throughput_threshold = 80 #percentage of send throughput required to pass the check 31 + #receive_throughput_threshold = 50 #percentage of receive throughput required to pass the check 32 + 33 + import time 34 + import json 35 + import argparse 36 + from lib.py import ksft_run, ksft_exit, ksft_pr, ksft_true 37 + from lib.py import KsftFailEx, KsftSkipEx, GenerateTraffic 38 + from lib.py import NetDrvEpEnv, bkg, wait_port_listen 39 + from lib.py import cmd 40 + from lib.py import LinkConfig 41 + 42 + class TestConfig: 43 + def __init__(self, time_delay: int, test_duration: int, send_throughput_threshold: int, receive_throughput_threshold: int) -> None: 44 + self.time_delay = time_delay 45 + self.test_duration = test_duration 46 + self.send_throughput_threshold = send_throughput_threshold 47 + self.receive_throughput_threshold = receive_throughput_threshold 48 + 49 + def _pre_test_checks(cfg: object, link_config: LinkConfig) -> None: 50 + if not link_config.verify_link_up(): 51 + KsftSkipEx(f"Link state of interface {cfg.ifname} is DOWN") 52 + common_link_modes = link_config.common_link_modes 53 + if common_link_modes is None: 54 + KsftSkipEx("No common link modes found") 55 + if link_config.partner_netif == None: 56 + KsftSkipEx("Partner interface is not available") 57 + if link_config.check_autoneg_supported(): 58 + KsftSkipEx("Auto-negotiation not supported by local") 59 + if link_config.check_autoneg_supported(remote=True): 60 + KsftSkipEx("Auto-negotiation not supported by remote") 61 + cfg.require_cmd("iperf3", remote=True) 62 + 63 + def check_throughput(cfg: object, link_config: LinkConfig, test_config: TestConfig, protocol: str, traffic: GenerateTraffic) -> None: 64 + common_link_modes = link_config.common_link_modes 65 + speeds, duplex_modes = link_config.get_speed_duplex_values(common_link_modes) 66 + """Test duration in seconds""" 67 + duration = test_config.test_duration 68 + 69 + ksft_pr(f"{protocol} test") 70 + test_type = "-u" if protocol == "UDP" else "" 71 + 72 + send_throughput = [] 73 + receive_throughput = [] 74 + for idx in range(0, len(speeds)): 75 + if link_config.set_speed_and_duplex(speeds[idx], duplex_modes[idx]) == False: 76 + raise KsftFailEx(f"Not able to set speed and duplex parameters for {cfg.ifname}") 77 + time.sleep(test_config.time_delay) 78 + if not link_config.verify_link_up(): 79 + raise KsftSkipEx(f"Link state of interface {cfg.ifname} is DOWN") 80 + 81 + send_command=f"{test_type} -b 0 -t {duration} --json" 82 + receive_command=f"{test_type} -b 0 -t {duration} --reverse --json" 83 + 84 + send_result = traffic.run_remote_test(cfg, command=send_command) 85 + if send_result.ret != 0: 86 + raise KsftSkipEx("Error occurred during data transmit: {send_result.stdout}") 87 + 88 + send_output = send_result.stdout 89 + send_data = json.loads(send_output) 90 + 91 + """Convert throughput to Mbps""" 92 + send_throughput.append(round(send_data['end']['sum_sent']['bits_per_second'] / 1e6, 2)) 93 + ksft_pr(f"{protocol}: Send throughput: {send_throughput[idx]} Mbps") 94 + 95 + receive_result = traffic.run_remote_test(cfg, command=receive_command) 96 + if receive_result.ret != 0: 97 + raise KsftSkipEx("Error occurred during data receive: {receive_result.stdout}") 98 + 99 + receive_output = receive_result.stdout 100 + receive_data = json.loads(receive_output) 101 + 102 + """Convert throughput to Mbps""" 103 + receive_throughput.append(round(receive_data['end']['sum_received']['bits_per_second'] / 1e6, 2)) 104 + ksft_pr(f"{protocol}: Receive throughput: {receive_throughput[idx]} Mbps") 105 + 106 + """Check whether throughput is not below the threshold (default values set at start)""" 107 + for idx in range(0, len(speeds)): 108 + send_threshold = float(speeds[idx]) * float(test_config.send_throughput_threshold / 100) 109 + receive_threshold = float(speeds[idx]) * float(test_config.receive_throughput_threshold / 100) 110 + ksft_true(send_throughput[idx] >= send_threshold, f"{protocol}: Send throughput is below threshold for {speeds[idx]} Mbps in {duplex_modes[idx]} duplex") 111 + ksft_true(receive_throughput[idx] >= receive_threshold, f"{protocol}: Receive throughput is below threshold for {speeds[idx]} Mbps in {duplex_modes[idx]} duplex") 112 + 113 + def test_tcp_throughput(cfg: object, link_config: LinkConfig, test_config: TestConfig, traffic: GenerateTraffic) -> None: 114 + _pre_test_checks(cfg, link_config) 115 + check_throughput(cfg, link_config, test_config, 'TCP', traffic) 116 + 117 + def test_udp_throughput(cfg: object, link_config: LinkConfig, test_config: TestConfig, traffic: GenerateTraffic) -> None: 118 + _pre_test_checks(cfg, link_config) 119 + check_throughput(cfg, link_config, test_config, 'UDP', traffic) 120 + 121 + def main() -> None: 122 + parser = argparse.ArgumentParser(description="Run basic performance test for NIC driver") 123 + parser.add_argument('--time-delay', type=int, default=8, help='Time taken to wait for transitions to happen(in seconds). Default is 8 seconds.') 124 + parser.add_argument('--test-duration', type=int, default=10, help='Performance test duration for the throughput check, in seconds. Default is 10 seconds.') 125 + parser.add_argument('--stt', type=int, default=80, help='Send throughput Threshold: Percentage of send throughput upon actual throughput required to pass the throughput check (in percentage). Default is 80.') 126 + parser.add_argument('--rtt', type=int, default=50, help='Receive throughput Threshold: Percentage of receive throughput upon actual throughput required to pass the throughput check (in percentage). Default is 50.') 127 + args=parser.parse_args() 128 + test_config = TestConfig(args.time_delay, args.test_duration, args.stt, args.rtt) 129 + with NetDrvEpEnv(__file__, nsim_test=False) as cfg: 130 + traffic = GenerateTraffic(cfg) 131 + link_config = LinkConfig(cfg) 132 + ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, link_config, test_config, traffic, )) 133 + link_config.reset_interface() 134 + ksft_exit() 135 + 136 + if __name__ == "__main__": 137 + main()
+19 -1
tools/testing/selftests/drivers/net/lib/py/load.py
··· 2 2 3 3 import time 4 4 5 - from lib.py import ksft_pr, cmd, ip, rand_port, wait_port_listen 5 + from lib.py import ksft_pr, cmd, ip, rand_port, wait_port_listen, bkg 6 6 7 7 class GenerateTraffic: 8 8 def __init__(self, env, port=None): ··· 22 22 if not self._wait_pkts(pps=1000): 23 23 self.stop(verbose=True) 24 24 raise Exception("iperf3 traffic did not ramp up") 25 + 26 + def run_remote_test(self, env: object, port=None, command=None): 27 + if port is None: 28 + port = rand_port() 29 + try: 30 + server_cmd = f"iperf3 -s 1 -p {port} --one-off" 31 + with bkg(server_cmd, host=env.remote): 32 + #iperf3 opens TCP connection as default in server 33 + #-u to be specified in client command for UDP 34 + wait_port_listen(port, host=env.remote) 35 + except Exception as e: 36 + raise Exception(f"Unexpected error occurred while running server command: {e}") 37 + try: 38 + client_cmd = f"iperf3 -c {env.remote_addr} -p {port} {command}" 39 + proc = cmd(client_cmd) 40 + return proc 41 + except Exception as e: 42 + raise Exception(f"Unexpected error occurred while running client command: {e}") 25 43 26 44 def _wait_pkts(self, pkt_cnt=None, pps=None): 27 45 """