[PATCH net-next V3 13/15] selftests: drv-net: gro: Extract shared helpers into gro_lib.py

From: Tariq Toukan

Date: Thu Sep 03 2026 - 05:40:27 EST


From: Cosmin Ratiu <cratiu@xxxxxxxxxx>

Bits and pieces of gro.py are needed by a second test file:
- dmac resolution
- the ethtool/sysfs feature dance
- the HW GRO setup logic
- the gro binary invocation
- the retry loop

Move them to gro_lib.py, installed via TEST_FILES (like psp_lib.py).

hw/gro_hw.py keeps its own copies for now since they diverged.

Signed-off-by: Cosmin Ratiu <cratiu@xxxxxxxxxx>
Reviewed-by: Dragos Tatulea <dtatulea@xxxxxxxxxx>
Signed-off-by: Tariq Toukan <tariqt@xxxxxxxxxx>
---
tools/testing/selftests/drivers/net/Makefile | 1 +
tools/testing/selftests/drivers/net/gro.py | 200 ++----------------
.../testing/selftests/drivers/net/gro_lib.py | 194 +++++++++++++++++
3 files changed, 217 insertions(+), 178 deletions(-)
create mode 100644 tools/testing/selftests/drivers/net/gro_lib.py

diff --git a/tools/testing/selftests/drivers/net/Makefile b/tools/testing/selftests/drivers/net/Makefile
index d68f9ab8955d..be77a15eaf38 100644
--- a/tools/testing/selftests/drivers/net/Makefile
+++ b/tools/testing/selftests/drivers/net/Makefile
@@ -28,6 +28,7 @@ TEST_PROGS := \
# end of TEST_PROGS

TEST_FILES := \
+ gro_lib.py \
psp_lib.py \
# end of TEST_FILES

diff --git a/tools/testing/selftests/drivers/net/gro.py b/tools/testing/selftests/drivers/net/gro.py
index 6ab8c97880d1..7111095bb479 100755
--- a/tools/testing/selftests/drivers/net/gro.py
+++ b/tools/testing/selftests/drivers/net/gro.py
@@ -37,90 +37,20 @@ Test cases:
"""

import glob
-import os
import re
from lib.py import ksft_run, ksft_exit, ksft_pr
-from lib.py import NetDrvEpEnv, KsftFailEx, KsftXfailEx
+from lib.py import NetDrvEpEnv, KsftXfailEx
from lib.py import NetdevFamily, EthtoolFamily
-from lib.py import bkg, cmd, defer, ethtool, ip
+from lib.py import defer, ethtool
from lib.py import ksft_variants, KsftNamedVariant
+from gro_lib import run_gro_bin, run_with_retries, set_ethtool_feat
+from gro_lib import set_mtu_restore, setup_hw_gro, write_defer_restore


# gro.c uses hardcoded DPORT=8000
GRO_DPORT = 8000


-def _resolve_dmac(cfg, ipver):
- """
- Find the destination MAC address remote host should use to send packets
- towards the local host. It may be a router / gateway address.
- """
-
- attr = "dmac" + ipver
- # Cache the response across test cases
- if hasattr(cfg, attr):
- return getattr(cfg, attr)
-
- route = ip(f"-{ipver} route get {cfg.addr_v[ipver]}",
- json=True, host=cfg.remote)[0]
- gw = route.get("gateway")
- # Local L2 segment, address directly
- if not gw:
- setattr(cfg, attr, cfg.dev['address'])
- return getattr(cfg, attr)
-
- # ping to make sure neighbor is resolved,
- # bind to an interface, for v6 the GW is likely link local
- cmd(f"ping -c1 -W0 -I{cfg.remote_ifname} {gw}", host=cfg.remote)
-
- neigh = ip(f"neigh get {gw} dev {cfg.remote_ifname}",
- json=True, host=cfg.remote)[0]
- setattr(cfg, attr, neigh['lladdr'])
- return getattr(cfg, attr)
-
-
-def _write_defer_restore(cfg, path, val, defer_undo=False):
- with open(path, "r", encoding="utf-8") as fp:
- orig_val = fp.read().strip()
- if str(val) == orig_val:
- return
- with open(path, "w", encoding="utf-8") as fp:
- fp.write(val)
- if defer_undo:
- defer(_write_defer_restore, cfg, path, orig_val)
-
-
-def _set_mtu_restore(dev, mtu, host):
- if dev['mtu'] < mtu:
- ip(f"link set dev {dev['ifname']} mtu {mtu}", host=host)
- defer(ip, f"link set dev {dev['ifname']} mtu {dev['mtu']}", host=host)
-
-
-def _set_ethtool_feat(dev, current, feats, host=None):
- s2n = {True: "on", False: "off"}
-
- new = ["-K", dev]
- old = ["-K", dev]
- no_change = True
- for name, state in feats.items():
- new += [name, s2n[state]]
- old += [name, s2n[current[name]["active"]]]
-
- if current[name]["active"] != state:
- no_change = False
- if current[name]["fixed"]:
- raise KsftXfailEx(f"Device does not support {name}")
- if no_change:
- return
-
- eth_cmd = ethtool(" ".join(new), host=host)
- defer(ethtool, " ".join(old), host=host)
-
- # If ethtool printed something kernel must have modified some features
- if eth_cmd.stdout:
- ksft_pr(eth_cmd)
-
-
def _get_queue_stats(cfg, queue_id):
"""Get stats for a specific Rx queue."""
cfg.wait_hw_stats_settle()
@@ -182,47 +112,6 @@ def _setup_queue_count(cfg, num_queues):
ethtool(f"-L {cfg.ifname} combined {num_queues}")


-def _run_gro_bin(cfg, test_name, protocol=None, num_flows=None,
- order_check=False, verbose=False, fail=False):
- """Run gro binary with given test and return the process result."""
- if not hasattr(cfg, "bin_remote"):
- cfg.bin_local = cfg.net_lib_dir / "gro"
- cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
-
- if protocol is None:
- ipver = cfg.addr_ipver
- protocol = f"ipv{ipver}"
- else:
- ipver = "6" if protocol[-1] == "6" else "4"
-
- dmac = _resolve_dmac(cfg, ipver)
-
- base_args = [
- f"--{protocol}",
- f"--dmac {dmac}",
- f"--smac {cfg.remote_dev['address']}",
- f"--daddr {cfg.addr_v[ipver]}",
- f"--saddr {cfg.remote_addr_v[ipver]}",
- f"--test {test_name}",
- ]
- if num_flows:
- base_args.append(f"--num-flows {num_flows}")
- if order_check:
- base_args.append("--order-check")
- if verbose:
- base_args.append("--verbose")
-
- args = " ".join(base_args)
-
- rx_cmd = f"{cfg.bin_local} {args} --rx --iface {cfg.ifname}"
- tx_cmd = f"{cfg.bin_remote} {args} --iface {cfg.remote_ifname}"
-
- with bkg(rx_cmd, ksft_ready=True, exit_wait=True, fail=fail) as rx_proc:
- cmd(tx_cmd, host=cfg.remote)
-
- return rx_proc
-
-
def _setup(cfg, mode, test_name):
""" Setup hardware loopback mode for GRO testing. """

@@ -237,60 +126,39 @@ def _setup(cfg, mode, test_name):

# "large_*" tests need at least 4k MTU
if test_name.startswith("large_"):
- _set_mtu_restore(cfg.dev, 4096, None)
- _set_mtu_restore(cfg.remote_dev, 4096, cfg.remote)
+ set_mtu_restore(cfg.dev, 4096, None)
+ set_mtu_restore(cfg.remote_dev, 4096, cfg.remote)

if mode == "sw":
flush_path = f"/sys/class/net/{cfg.ifname}/gro_flush_timeout"
irq_path = f"/sys/class/net/{cfg.ifname}/napi_defer_hard_irqs"

- _write_defer_restore(cfg, flush_path, "200000", defer_undo=True)
- _write_defer_restore(cfg, irq_path, "10", defer_undo=True)
+ write_defer_restore(cfg, flush_path, "200000", defer_undo=True)
+ write_defer_restore(cfg, irq_path, "10", defer_undo=True)

- _set_ethtool_feat(cfg.ifname, cfg.feat,
- {"generic-receive-offload": True,
- "rx-gro-hw": False,
- "large-receive-offload": False})
+ set_ethtool_feat(cfg.ifname, cfg.feat,
+ {"generic-receive-offload": True,
+ "rx-gro-hw": False,
+ "large-receive-offload": False})
elif mode == "hw":
- _set_ethtool_feat(cfg.ifname, cfg.feat,
- {"generic-receive-offload": False,
- "rx-gro-hw": True,
- "large-receive-offload": False})
-
- # Some NICs treat HW GRO as a GRO sub-feature so disabling GRO
- # will also clear HW GRO. Use a hack of installing XDP generic
- # to skip SW GRO, even when enabled.
- feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
- if not feat["rx-gro-hw"]["active"]:
- ksft_pr("Driver clears HW GRO and SW GRO is cleared, using generic XDP workaround")
- prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
- ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp")
- defer(ip, f"link set dev {cfg.ifname} xdpgeneric off")
-
- # Attaching XDP may change features, fetch the latest state
- feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
-
- _set_ethtool_feat(cfg.ifname, feat,
- {"generic-receive-offload": True,
- "rx-gro-hw": True,
- "large-receive-offload": False})
+ setup_hw_gro(cfg)
elif mode == "lro":
# netdevsim advertises LRO for feature inheritance testing with
# bonding/team tests but it doesn't actually perform the offload
cfg.require_nsim(nsim_test=False)

- _set_ethtool_feat(cfg.ifname, cfg.feat,
- {"generic-receive-offload": False,
- "rx-gro-hw": False,
- "large-receive-offload": True})
+ set_ethtool_feat(cfg.ifname, cfg.feat,
+ {"generic-receive-offload": False,
+ "rx-gro-hw": False,
+ "large-receive-offload": True})

try:
# Disable TSO for local tests
cfg.require_nsim() # will raise KsftXfailEx if not running on nsim

- _set_ethtool_feat(cfg.remote_ifname, cfg.remote_feat,
- {"tcp-segmentation-offload": False},
- host=cfg.remote)
+ set_ethtool_feat(cfg.remote_ifname, cfg.remote_feat,
+ {"tcp-segmentation-offload": False},
+ host=cfg.remote)
except KsftXfailEx:
pass

@@ -356,31 +224,7 @@ def test(cfg, mode, protocol, test_name):

_setup(cfg, mode, test_name)

- # Each test is run 6 times to deflake, because given the receive timing,
- # not all packets that should coalesce will be considered in the same flow
- # on every try.
- max_retries = 6
- for attempt in range(max_retries):
- fail_now = attempt >= max_retries - 1
- rx_proc = _run_gro_bin(cfg, test_name, protocol=protocol,
- verbose=True, fail=fail_now)
-
- if rx_proc.ret == 0:
- return
-
- ksft_pr(rx_proc)
-
- # ret==42 means the receiver detected over-coalescing.
- # This is unambiguous proof of a bug, retries can only cause
- # false negatives.
- if rx_proc.ret == 42:
- raise KsftFailEx(f"GRO over-coalesced in {protocol}/{test_name}")
-
- if test_name.startswith("large_") and os.environ.get("KSFT_MACHINE_SLOW"):
- ksft_pr(f"Ignoring {protocol}/{test_name} failure due to slow environment")
- return
-
- ksft_pr(f"Attempt {attempt + 1}/{max_retries} failed, retrying...")
+ run_with_retries(cfg, test_name, protocol=protocol, verbose=True)


def _capacity_variants():
@@ -420,7 +264,7 @@ def test_gro_capacity(cfg, mode, setup_func):
if queue_id is not None:
stats_before = _get_queue_stats(cfg, queue_id)

- rx_proc = _run_gro_bin(cfg, "capacity", num_flows=num_flows)
+ rx_proc = run_gro_bin(cfg, "capacity", num_flows=num_flows)
output = rx_proc.stdout

if queue_id is not None:
diff --git a/tools/testing/selftests/drivers/net/gro_lib.py b/tools/testing/selftests/drivers/net/gro_lib.py
new file mode 100644
index 000000000000..4e3d704397fe
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/gro_lib.py
@@ -0,0 +1,194 @@
+# SPDX-License-Identifier: GPL-2.0
+
+"""Shared helpers for the GRO selftests."""
+
+import os
+
+from lib.py import ksft_pr
+from lib.py import KsftFailEx, KsftXfailEx
+from lib.py import bkg, cmd, defer, ethtool, ip
+
+
+# gro.c exits with this code when it detects over-coalescing
+EXIT_OVER_COALESCE = 42
+
+
+def resolve_dmac(cfg, ipver):
+ """
+ Finds the destination MAC address remote host should use to send packets
+ towards the local host. It may be a router / gateway address.
+ """
+
+ attr = "dmac" + ipver
+ # Cache the response across test cases
+ if hasattr(cfg, attr):
+ return getattr(cfg, attr)
+
+ route = ip(f"-{ipver} route get {cfg.addr_v[ipver]}",
+ json=True, host=cfg.remote)[0]
+ gw = route.get("gateway")
+ # Local L2 segment, address directly
+ if not gw:
+ setattr(cfg, attr, cfg.dev['address'])
+ return getattr(cfg, attr)
+
+ # ping to make sure neighbor is resolved,
+ # bind to an interface, for v6 the GW is likely link local
+ cmd(f"ping -c1 -W0 -I{cfg.remote_ifname} {gw}", host=cfg.remote)
+
+ neigh = ip(f"neigh get {gw} dev {cfg.remote_ifname}",
+ json=True, host=cfg.remote)[0]
+ setattr(cfg, attr, neigh['lladdr'])
+ return getattr(cfg, attr)
+
+
+def write_defer_restore(cfg, path, val, defer_undo=False):
+ """Writes val to a sysfs file, optionally restoring it on test exit."""
+ with open(path, "r", encoding="utf-8") as fp:
+ orig_val = fp.read().strip()
+ if str(val) == orig_val:
+ return
+ with open(path, "w", encoding="utf-8") as fp:
+ fp.write(val)
+ if defer_undo:
+ defer(write_defer_restore, cfg, path, orig_val)
+
+
+def set_mtu_restore(dev, mtu, host):
+ """Raises a device's MTU to at least mtu, restoring it on test exit."""
+ if dev['mtu'] < mtu:
+ ip(f"link set dev {dev['ifname']} mtu {mtu}", host=host)
+ defer(ip, f"link set dev {dev['ifname']} mtu {dev['mtu']}", host=host)
+
+
+def set_ethtool_feat(dev, current, feats, host=None):
+ """Sets ethtool features, restoring them on test exit.
+
+ current is the feature state as reported by "ethtool -k", xfail if a
+ feature which needs changing is fixed.
+ """
+ s2n = {True: "on", False: "off"}
+
+ new = ["-K", dev]
+ old = ["-K", dev]
+ no_change = True
+ for name, state in feats.items():
+ new += [name, s2n[state]]
+ old += [name, s2n[current[name]["active"]]]
+
+ if current[name]["active"] != state:
+ no_change = False
+ if current[name]["fixed"]:
+ raise KsftXfailEx(f"Device does not support {name}")
+ if no_change:
+ return
+
+ eth_cmd = ethtool(" ".join(new), host=host)
+ defer(ethtool, " ".join(old), host=host)
+
+ # If ethtool printed something kernel must have modified some features
+ if eth_cmd.stdout:
+ ksft_pr(eth_cmd)
+
+
+def setup_hw_gro(cfg):
+ """Turns on HW GRO and make sure SW GRO stays out of the way.
+
+ Expects cfg.feat to hold the local device's "ethtool -k" state.
+ """
+ set_ethtool_feat(cfg.ifname, cfg.feat,
+ {"generic-receive-offload": False,
+ "rx-gro-hw": True,
+ "large-receive-offload": False})
+
+ # Some NICs treat HW GRO as a GRO sub-feature so disabling GRO
+ # will also clear HW GRO. Use a hack of installing XDP generic
+ # to skip SW GRO, even when enabled.
+ feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
+ if not feat["rx-gro-hw"]["active"]:
+ ksft_pr("Driver clears HW GRO and SW GRO is cleared, using generic XDP workaround")
+ prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
+ ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp")
+ defer(ip, f"link set dev {cfg.ifname} xdpgeneric off")
+
+ # Attaching XDP may change features, fetch the latest state
+ feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
+
+ set_ethtool_feat(cfg.ifname, feat,
+ {"generic-receive-offload": True,
+ "rx-gro-hw": True,
+ "large-receive-offload": False})
+
+
+# pylint: disable=too-many-arguments,too-many-positional-arguments
+def run_gro_bin(cfg, test_name, protocol=None, num_flows=None,
+ order_check=False, verbose=False, fail=False):
+ """Runs gro binary with given test and return the process result."""
+ if not hasattr(cfg, "bin_remote"):
+ cfg.bin_local = cfg.net_lib_dir / "gro"
+ cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
+
+ if protocol is None:
+ ipver = cfg.addr_ipver
+ protocol = f"ipv{ipver}"
+ else:
+ ipver = "6" if protocol[-1] == "6" else "4"
+
+ dmac = resolve_dmac(cfg, ipver)
+
+ base_args = [
+ f"--{protocol}",
+ f"--dmac {dmac}",
+ f"--smac {cfg.remote_dev['address']}",
+ f"--daddr {cfg.addr_v[ipver]}",
+ f"--saddr {cfg.remote_addr_v[ipver]}",
+ f"--test {test_name}",
+ ]
+ if num_flows:
+ base_args.append(f"--num-flows {num_flows}")
+ if order_check:
+ base_args.append("--order-check")
+ if verbose:
+ base_args.append("--verbose")
+
+ args = " ".join(base_args)
+
+ rx_cmd = f"{cfg.bin_local} {args} --rx --iface {cfg.ifname}"
+ tx_cmd = f"{cfg.bin_remote} {args} --iface {cfg.remote_ifname}"
+
+ with bkg(rx_cmd, ksft_ready=True, exit_wait=True, fail=fail) as rx_proc:
+ cmd(tx_cmd, host=cfg.remote)
+
+ return rx_proc
+
+
+def run_with_retries(cfg, test_name, protocol=None, **kwargs):
+ """Runs a single gro test case, retrying to deflake it.
+
+ Each test is run 6 times, because given the receive timing, not all
+ packets that should coalesce will be considered in the same flow on
+ every try. Over-coalescing is a hard failure, retries can only
+ cause false negatives there.
+ """
+ max_retries = 6
+ for attempt in range(max_retries):
+ fail_now = attempt >= max_retries - 1
+ rx_proc = run_gro_bin(cfg, test_name, protocol=protocol,
+ fail=fail_now, **kwargs)
+
+ if rx_proc.ret == 0:
+ return
+
+ ksft_pr(rx_proc)
+
+ # ret==42 means the receiver detected over-coalescing.
+ # This is unambiguous proof of a bug, retries can only cause
+ # false negatives.
+ if rx_proc.ret == EXIT_OVER_COALESCE:
+ raise KsftFailEx(f"GRO over-coalesced in {protocol}/{test_name}")
+
+ if test_name.startswith("large_") and os.environ.get("KSFT_MACHINE_SLOW"):
+ ksft_pr(f"Ignoring {protocol}/{test_name} failure due to slow environment")
+ return
+
+ ksft_pr(f"Attempt {attempt + 1}/{max_retries} failed, retrying...")
--
2.44.0