diff options
| -rw-r--r-- | tools/testing/selftests/drivers/net/hw/Makefile | 1 | ||||
| -rwxr-xr-x | tools/testing/selftests/drivers/net/hw/pause.py | 369 |
2 files changed, 370 insertions, 0 deletions
diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile index 18ce235a77f8..9b6b5d15f04d 100644 --- a/tools/testing/selftests/drivers/net/hw/Makefile +++ b/tools/testing/selftests/drivers/net/hw/Makefile @@ -40,6 +40,7 @@ TEST_PROGS = \ nk_netns.py \ nk_qlease.py \ ntuple.py \ + pause.py \ pp_alloc_fail.py \ rss_api.py \ rss_ctx.py \ diff --git a/tools/testing/selftests/drivers/net/hw/pause.py b/tools/testing/selftests/drivers/net/hw/pause.py new file mode 100755 index 000000000000..854428e19f55 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/pause.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +""" +Driver-related behavior tests for Pause-based Flow Control. +""" + +import errno +import json +import os + +from lib.py import KsftFailEx, KsftNamedVariant, KsftSkipEx +from lib.py import NetDrvEpEnv +from lib.py import cmd, defer, ethtool, ip, wait_file +from lib.py import ksft_disruptive, ksft_variants, ksft_run +from lib.py import ksft_eq, ksft_exit, ksft_in, ksft_not_in, ksft_pr + +# Linkmodes to Pause params : +# Pause bit is set if rx == 1 +# Asym_Pause bit is set if rx != tx +pauseparams_to_linkmodes = { + 0: {0: {"rx": 0, "tx": 0, "linkmodes": []}, + 1: {"rx": 0, "tx": 1, "linkmodes": ["Asym_Pause"]}}, + 1: {0: {"rx": 1, "tx": 0, "linkmodes": ["Pause", "Asym_Pause"]}, + 1: {"rx": 1, "tx": 1, "linkmodes": ["Pause"]}}, +} + +def onoff(val): + """ Convert a bool to on/off """ + return "on" if val else "off" + +pauseparams_variants = [ + KsftNamedVariant(f"RX {onoff(p['rx'])} TX {onoff(p['tx'])}", p) + for by_tx in pauseparams_to_linkmodes.values() for p in by_tx.values() +] + +_strerrors = {os.strerror(e): e for e in errno.errorcode} + +def ethtool_ret(command, is_get=True, host=None): + """ Execute an ethtool command, returns the return code and JSON content + + :param command: the ethtool arguments + :param is_get: Is the command a get or a set. Get commands return the loaded + JSON attributes + :param host: The host on which to run the command on. None means local host. + """ + json_flag = "--json" if is_get else "" + cmd_res = cmd(f"ethtool {json_flag} {command}", host=host, fail=False) + + if cmd_res.ret != 0: + # ethtool returns 1 upon error, not the netlink errcode. Try to get it + # by parsing the stderr output, which looks like : + # "netlink error: Operation not supported" + for line in cmd_res.stderr.splitlines(): + err = _strerrors.get(line.rsplit(": ", 1)[-1].strip()) + if err: + return err, None + return cmd_res.ret, None + + # Not a get operation, we don't have any JSON output to parse + if not is_get: + return 0, None + + return 0, json.loads(cmd_res.stdout)[0] + +def wait_for_link(cfg, timeout=15): + """ Wait for the link to come up """ + + wait_file(f"/sys/class/net/{cfg.ifname}/carrier", + lambda x: x.strip() == "1", deadline=timeout) + +def wait_for_aneg(cfg, link_drop=False, timeout=15): + """ Wait for a renegotiation to complete + + :param link_drop: Set to true if the link HAS to flap. + """ + try: + wait_file(f"/sys/class/net/{cfg.ifname}/carrier", + lambda x: x.strip() == "0", + deadline=timeout if link_drop else 2) + except TimeoutError: + if link_drop: + raise + + wait_for_link(cfg, timeout) + +def forced_link_settings(cfg): + """ Returns a string to pass to ethtool -s with speed/duplex corresponding + to the current settings. + + Note that some devices don't return duplex info, so assume full duplex + in that case. + """ + link = ethtool(f"{cfg.ifname}", json=True)[0] + if "speed" not in link: + return "" + + return f"speed {link['speed']} duplex {link.get('duplex', 'Full').lower()}" + +def _ethtool_pause_use_to_linkmodes(use): + """ Convert the ethtool output for pause modes into pause linkmodes """ + if use == "Symmetric": + return ["Pause"] + elif use == "Symmetric Receive-only": + return ["Pause", "Asym_Pause"] + elif use == "Transmit-only": + return ["Asym_Pause"] + else: + return [] + +def set_local_pauseparams(cfg, rx, tx, aneg): + """ set pauseparams : ethtool -A """ + rx_param = onoff(rx) + tx_param = onoff(tx) + aneg_param = onoff(aneg) + + ret, _ = ethtool_ret(f"-A {cfg.ifname} rx {rx_param} tx {tx_param}" + f" autoneg {aneg_param}", + is_get = False) + + if ret != 0 and ret != errno.EOPNOTSUPP: + raise KsftFailEx(f"ethtool -A {cfg.ifname} failed: {ret}") + + return ret + +def get_local_pauseparams(cfg): + """ get pauseparams : ethtool -a """ + return ethtool_ret(f"-a {cfg.ifname}", is_get=True) + +def get_local_pause_supported(cfg): + """ Return the errcode and the local pause supported linkmodes """ + + ret, data = ethtool_ret(f"{cfg.ifname}") + if ret != 0: + raise KsftFailEx(f"ethtool {cfg.ifname} failed: {ret}") + + if "supported-pause-frame-use" not in data: + raise KsftSkipEx(f"{cfg.ifname} doesn't report its link modes") + + return ret, _ethtool_pause_use_to_linkmodes(data["supported-pause-frame-use"]) + +def get_local_pause_advertising(cfg): + """ Return the errcode and the local pause advertised linkmodes """ + + ret, data = ethtool_ret(f"{cfg.ifname}") + if ret != 0: + raise KsftFailEx(f"ethtool {cfg.ifname} failed: {ret}") + + if "advertised-pause-frame-use" not in data: + raise KsftSkipEx(f"{cfg.ifname} doesn't report its link modes") + + return ret, _ethtool_pause_use_to_linkmodes(data["advertised-pause-frame-use"]) + +def require_pause_supported_allof(cfg, linkmodes): + """ Skip if local device doesn't support all of the passed modes """ + + ret, _ = get_local_pauseparams(cfg) + if ret != 0: + raise KsftSkipEx("device doesn't allow getting pauseparams") + + _, pause_support = get_local_pause_supported(cfg) + for lm in linkmodes: + if lm not in pause_support: + raise KsftSkipEx(f"Local device doesn't support {lm}") + +def expect_pauseparams_set(cfg, pauseparams, aneg, supported, note): + """ ethtool -A must work or be refused depending on what the local + device supports + """ + rx = pauseparams["rx"] + tx = pauseparams["tx"] + + # Measure current pauseparams, re-setting the same params as the current + # ones returns 0 even if unsupported. + _, cur = get_local_pauseparams(cfg) + noop = cur and (cur["rx"], cur["tx"], cur["autonegotiate"]) == \ + (bool(rx), bool(tx), aneg) + + ret, _ = ethtool_ret(f"-A {cfg.ifname} rx {onoff(rx)} tx {onoff(tx)}" + f" autoneg {onoff(aneg)}", is_get = False) + + if noop: + ksft_eq(ret, 0, note) + elif not supported: + ksft_eq(ret, errno.EOPNOTSUPP, note) + elif set(pauseparams["linkmodes"]).issubset(set(supported)): + ksft_eq(ret, 0, note) + else: + ksft_in(ret, (errno.EOPNOTSUPP, errno.EINVAL), note) + +def pause_setup(cfg): + """ The starting conditions every test counts on, restored on exit: + - both ports admin up + - link autoneg on on both sides ifsupported + - carrier on + """ + + # Get init pause parameters + ret, params = ethtool_ret(f"-a {cfg.ifname}") + if ret == 0: + defer(cmd, f"ethtool -A {cfg.ifname} rx {onoff(params['rx'])} " + f"tx {onoff(params['tx'])} " + f"autoneg {onoff(params['autonegotiate'])}") + + # Get init link parameters + link = ethtool(f"{cfg.ifname}", json=True)[0] + if "auto-negotiation" not in link: + raise KsftSkipEx(f"{cfg.ifname} doesn't report its link modes") + + if link["auto-negotiation"]: + defer(cmd, f"ethtool -s {cfg.ifname} autoneg on") + elif "speed" in link and "duplex" in link: + defer(cmd, f"ethtool -s {cfg.ifname} autoneg off speed {link['speed']} " + f"duplex {link['duplex'].lower()}") + + # Local interface admin up, link aneg on + ip(f"link set {cfg.ifname} up") + if link["supports-auto-negotiation"] and not link["auto-negotiation"]: + ethtool(f"-s {cfg.ifname} autoneg on") + + # Get remote pause params + ret, params = ethtool_ret(f"-a {cfg.remote_ifname}", host=cfg.remote) + if ret == 0: + defer(cmd, f"ethtool -A {cfg.remote_ifname} rx {onoff(params['rx'])} " + f"tx {onoff(params['tx'])} " + f"autoneg {onoff(params['autonegotiate'])}", + host=cfg.remote) + + # Get remote link params + link = ethtool(f"{cfg.remote_ifname}", json=True, host=cfg.remote)[0] + if "auto-negotiation" not in link: + raise KsftSkipEx(f"{cfg.remote_ifname} doesn't report its link modes") + + if link["auto-negotiation"]: + defer(cmd, f"ethtool -s {cfg.remote_ifname} autoneg on", + host=cfg.remote) + elif "speed" in link and "duplex" in link: + defer(cmd, f"ethtool -s {cfg.remote_ifname} autoneg off " + f"speed {link['speed']} duplex {link['duplex'].lower()}", + host=cfg.remote) + + # Remote interface admin up, link aneg on + ip(f"link set {cfg.remote_ifname} up", host=cfg.remote) + if link["supports-auto-negotiation"] and not link["auto-negotiation"]: + ethtool(f"-s {cfg.remote_ifname} autoneg on", host=cfg.remote) + + wait_for_link(cfg) + +# Pause support : Supported linkmodes vs ability to set/get pauseparams +@ksft_variants(pauseparams_variants) +@ksft_disruptive +def pause_test_support(cfg, pauseparams): + """ Verify that the supported linkmodes Pause and Asym_Pause match the + ability to configure the rx and tx pauseparams. + + Drivers are expected to reject pauseparams they don't support, and + accept the ones they support. The supported modes are exposed by + the MAC to the PHY layer through phylink mac_capabilities MAC_SYM_PAUSE + and MAC_ASYM_PAUSE, or through phylib directly with the + phy_support_sym_pause() and phy_support_asym_pause() helpers. + + The expectation is for drivers to refuse setting pauseparams that don't + match the Pause and Asym_Pause bits in the supported linkmodes with a + -EOPNOTSUPP return value. Unsupported pause params must be rejected. + + Failing this test likely means the MAC driver doesn't implement the + set/get_pauseparam, but still sets flow control as supported through + phylink mac_capabilities or phylib's pause API. Conversely, the MAC driver + may have omitted to indicate its supported Pause modes. Finally, the PHY + driver may incorrectly override the Pause and Asym_Pause bits in its + supported fields. + """ + + pause_setup(cfg) + + forced = forced_link_settings(cfg) + _, supported = get_local_pause_supported(cfg) + + # We check that what we can configure in the pause params matches what we + # support under various contditions : Link aneg on/off, pause aneg on/off + ret, _ = ethtool_ret(f"-s {cfg.ifname} autoneg on", is_get = False) + if ret != 0: + ksft_pr("link autoneg on refused, not tested") + else: + expect_pauseparams_set(cfg, pauseparams, False, supported, + "link autoneg on") + expect_pauseparams_set(cfg, pauseparams, True, supported, + "link autoneg on") + + if not forced: + ksft_pr("link speed unknown, the forced link is not tested") + return + + ret, _ = ethtool_ret(f"-s {cfg.ifname} autoneg off {forced}", is_get = False) + if ret != 0: + ksft_pr(f"link autoneg off {forced} refused, not tested") + return + + expect_pauseparams_set(cfg, pauseparams, False, supported, "link autoneg off") + expect_pauseparams_set(cfg, pauseparams, True, supported, "link autoneg off") + +@ksft_variants(pauseparams_variants) +@ksft_disruptive +def pause_advertising_test(cfg, pauseparams): + """Pause advertisement + + Validate that changing pause params through the ETHTOOL_MSG_PAUSE command + translates to a change in the advertised pause params, and that these + parameters are correct w.r.t the supported pause params and requested pause + params. + + This exercises the .set_pauseparam() ethtool ops for MAC configuration, + as well as the reconfiguration of the PHY's advertising and negotiation. + + On non-phylink MACs, the MAC should call phy_set_sym_pause() to update the + PHY's advertising, and restart a negotiation with phy_start_aneg() if + need be. Failure to do so will result in the wrong advertising parameters. + + On phylink-enabled MACs, phylink deals with the PHY reconfiguration provided + the MAC driver calls phylink_ethtool_set_pauseparam(). + + Failing this test likely means that the PHY driver is not correctly + advertising pause settings, either due to the MAC not triggering a PHY + reconfiguration, a misconfiguration of the advertising registers by the PHY, + or by mis-handling the phydev->advertising bitmap in the PHY driver directly. + + The validation is made by looking at the advertised modes locally, as well + as what the peer's 'lp_advertising' values report. + """ + + require_pause_supported_allof(cfg, pauseparams["linkmodes"]) + pause_setup(cfg) + + tx = pauseparams["tx"] + rx = pauseparams["rx"] + adv = pauseparams["linkmodes"] + not_adv = [ l for l in ["Pause", "Asym_Pause"] if l not in adv] + + # It's OK to skip here, we're already validating the EOPNOTSUPP behaviour + # the pause_test_support test. + ret = set_local_pauseparams(cfg, rx, tx, True) + if ret == errno.EOPNOTSUPP: + raise KsftSkipEx(f"RX {rx} TX {tx} not supported") + + # Wait for link parameters to re-negotiate and link to come back up. It must + # come back up, otherwise that means changing pauseparams can bring the + # link down. + wait_for_aneg(cfg) + + _, linkmodes = get_local_pause_advertising(cfg) + for mode in adv: + ksft_in(mode, linkmodes, + f"rx {rx} tx {tx} aneg on must advertise {adv}") + + for mode in not_adv: + ksft_not_in(mode, linkmodes, + f"rx {rx} tx {tx} aneg on must not advertise {not_adv}") + +def main(): + """ Entry point """ + with NetDrvEpEnv(__file__, nsim_test=False) as cfg: + ksft_run([pause_test_support, + pause_advertising_test, + ], + args=(cfg, )) + ksft_exit() + +if __name__ == "__main__": + main() |
