[PATCH net-next v2 3/3] selftests: rss_drv: Add RSS indirection table resize tests

From: Björn Töpel

Date: Wed Mar 04 2026 - 08:25:09 EST


Add resize tests to rss_drv.py. Devices without dynamic table sizing
are skipped via _require_dynamic_indir_size().

resize_periodic: set a periodic table (equal 4), shrink channels to
fold, grow back to unfold. Check the exact pattern is preserved. Has
main and non-default context variants.

resize_nonperiodic_reject: set a non-periodic table (equal N), verify
that channel reduction is rejected.

Signed-off-by: Björn Töpel <bjorn@xxxxxxxxxx>
---
.../selftests/drivers/net/hw/rss_drv.py | 163 +++++++++++++++++-
1 file changed, 159 insertions(+), 4 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/hw/rss_drv.py b/tools/testing/selftests/drivers/net/hw/rss_drv.py
index 2d1a33189076..9763760f8306 100755
--- a/tools/testing/selftests/drivers/net/hw/rss_drv.py
+++ b/tools/testing/selftests/drivers/net/hw/rss_drv.py
@@ -5,9 +5,9 @@
Driver-related behavior tests for RSS.
"""

-from lib.py import ksft_run, ksft_exit, ksft_ge
-from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx
-from lib.py import defer, ethtool
+from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge
+from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx, ksft_raises
+from lib.py import defer, ethtool, CmdExitFailure
from lib.py import EthtoolFamily, NlError
from lib.py import NetDrvEnv

@@ -45,6 +45,18 @@ def _maybe_create_context(cfg, create_context):
return ctx_id


+def _require_dynamic_indir_size(cfg, ch_max):
+ """Skip if the device does not dynamically size its indirection table."""
+ ethtool(f"-X {cfg.ifname} default")
+ ethtool(f"-L {cfg.ifname} combined 2")
+ small = len(_get_rss(cfg)['rss-indirection-table'])
+ ethtool(f"-L {cfg.ifname} combined {ch_max}")
+ large = len(_get_rss(cfg)['rss-indirection-table'])
+
+ if small == large:
+ raise KsftSkipEx("Device does not dynamically size indirection table")
+
+
@ksft_variants([
KsftNamedVariant("main", False),
KsftNamedVariant("ctx", True),
@@ -76,11 +88,154 @@ def indir_size_4x(cfg, create_context):
_test_rss_indir_size(cfg, test_max, context=ctx_id)


+@ksft_variants([
+ KsftNamedVariant("main", False),
+ KsftNamedVariant("ctx", True),
+])
+def resize_periodic(cfg, create_context):
+ """Test that a periodic indirection table survives channel changes.
+
+ Set a non-default periodic table ([3, 2, 1, 0] x N) via netlink,
+ reduce channels to trigger a fold, then increase to trigger an
+ unfold. Using a reversed pattern (instead of [0, 1, 2, 3]) ensures
+ the test can distinguish a correct fold from a driver that silently
+ resets the table to defaults. Verify the exact pattern is preserved
+ and the size tracks the channel count.
+ """
+ channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
+ ch_max = channels.get('combined-max', 0)
+ qcnt = channels['combined-count']
+
+ if ch_max < 4:
+ raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
+
+ defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
+
+ _require_dynamic_indir_size(cfg, ch_max)
+
+ ctx_id = _maybe_create_context(cfg, create_context)
+
+ # Set a non-default periodic pattern via netlink
+ rss = _get_rss(cfg, context=ctx_id)
+ orig_size = len(rss['rss-indirection-table'])
+ pattern = [3, 2, 1, 0] * (orig_size // 4)
+ req = {'header': {'dev-index': cfg.ifindex}, 'indir': pattern}
+ if ctx_id:
+ req['context'] = ctx_id
+ else:
+ defer(ethtool, f"-X {cfg.ifname} default")
+ cfg.ethnl.rss_set(req)
+
+ # Shrink — should fold
+ ethtool(f"-L {cfg.ifname} combined 4")
+ rss = _get_rss(cfg, context=ctx_id)
+ indir = rss['rss-indirection-table']
+
+ ksft_ge(orig_size, len(indir), "Table did not shrink")
+ ksft_eq(indir, [3, 2, 1, 0] * (len(indir) // 4),
+ "Folded table has wrong pattern")
+
+ # Grow back — should unfold
+ ethtool(f"-L {cfg.ifname} combined {ch_max}")
+ rss = _get_rss(cfg, context=ctx_id)
+ indir = rss['rss-indirection-table']
+
+ ksft_eq(len(indir), orig_size, "Table size not restored")
+ ksft_eq(indir, [3, 2, 1, 0] * (len(indir) // 4),
+ "Unfolded table has wrong pattern")
+
+
+@ksft_variants([
+ KsftNamedVariant("main", False),
+ KsftNamedVariant("ctx", True),
+])
+def resize_nonperiodic_reject(cfg, create_context):
+ """Test that a non-periodic table blocks channel reduction.
+
+ Set equal weight across all queues so the table is not periodic
+ at any smaller size, then verify channel reduction is rejected.
+ An additional context with a periodic table is created to verify
+ that validation catches the non-periodic one even when others
+ are fine.
+ """
+ channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
+ ch_max = channels.get('combined-max', 0)
+ qcnt = channels['combined-count']
+
+ if ch_max < 4:
+ raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
+
+ defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
+
+ _require_dynamic_indir_size(cfg, ch_max)
+
+ ctx_id = _maybe_create_context(cfg, create_context)
+ ctx_ref = f"context {ctx_id}" if ctx_id else ""
+
+ # Create an extra context with a periodic (foldable) table so that
+ # the validation must iterate all contexts to find the bad one.
+ extra_ctx = _maybe_create_context(cfg, True)
+ ethtool(f"-X {cfg.ifname} context {extra_ctx} equal 2")
+
+ ethtool(f"-X {cfg.ifname} {ctx_ref} equal {ch_max}")
+ if not create_context:
+ defer(ethtool, f"-X {cfg.ifname} default")
+
+ with ksft_raises(CmdExitFailure):
+ ethtool(f"-L {cfg.ifname} combined 2")
+
+
+@ksft_variants([
+ KsftNamedVariant("main", False),
+ KsftNamedVariant("ctx", True),
+])
+def resize_nonperiodic_no_corruption(cfg, create_context):
+ """Test that a failed resize does not corrupt table or channel count.
+
+ Set a non-periodic table, attempt a channel reduction (which must
+ fail), then verify both the indirection table contents and the
+ channel count are unchanged.
+ """
+ channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
+ ch_max = channels.get('combined-max', 0)
+ qcnt = channels['combined-count']
+
+ if ch_max < 4:
+ raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
+
+ defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
+
+ _require_dynamic_indir_size(cfg, ch_max)
+
+ ctx_id = _maybe_create_context(cfg, create_context)
+ ctx_ref = f"context {ctx_id}" if ctx_id else ""
+
+ ethtool(f"-X {cfg.ifname} {ctx_ref} equal {ch_max}")
+ if not create_context:
+ defer(ethtool, f"-X {cfg.ifname} default")
+
+ rss_before = _get_rss(cfg, context=ctx_id)
+
+ with ksft_raises(CmdExitFailure):
+ ethtool(f"-L {cfg.ifname} combined 2")
+
+ rss_after = _get_rss(cfg, context=ctx_id)
+ ksft_eq(rss_after['rss-indirection-table'],
+ rss_before['rss-indirection-table'],
+ "Indirection table corrupted after failed resize")
+
+ channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
+ ksft_eq(channels['combined-count'], ch_max,
+ "Channel count changed after failed resize")
+
+
def main() -> None:
""" Ksft boiler plate main """
with NetDrvEnv(__file__) as cfg:
cfg.ethnl = EthtoolFamily()
- ksft_run([indir_size_4x], args=(cfg, ))
+ ksft_run([indir_size_4x, resize_periodic,
+ resize_nonperiodic_reject,
+ resize_nonperiodic_no_corruption], args=(cfg, ))
ksft_exit()


--
2.53.0