From 9515a13829dfa98e3c462e179337e7b9cc3ca841 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:23 -0700 Subject: [PATCH 01/14] selftests: net: shaper: Drop redundant command timeouts Commit 57bb59ab6fa3 ("selftests: net: bump default cmd() timeout to 20 seconds") raised the default cmd() timeout to 20 seconds, so the explicit timeout=10 passed to the ethtool channel commands in queue_update() is now redundant and, in fact, shorter than the default. Drop it and rely on the default timeout. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-2-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index e39d270e688d..c80a4bf8cc05 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -388,7 +388,7 @@ def queue_update(cfg, nl_shaper) -> None: 'bw-max': (i + 1) * 1000}) # Delete a channel, with no shapers configured on top of the related # queue: no changes expected - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} 3", timeout=10) + cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} 3") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, @@ -408,7 +408,7 @@ def queue_update(cfg, nl_shaper) -> None: # Delete a channel, with a shaper configured on top of the related # queue: the shaper must be deleted, too - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} 2", timeout=10) + cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} 2") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, @@ -423,7 +423,7 @@ def queue_update(cfg, nl_shaper) -> None: 'bw-max': 2000}]) # Restore the original channels number, no expected changes - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} {cfg.nr_queues}", timeout=10) + cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} {cfg.nr_queues}") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, From 7ea7db704f51c71253996fa8ee19670516d61f67 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:24 -0700 Subject: [PATCH 02/14] selftests: net: shaper: Prepare helpers for group tests dup_leaves expects the kernel to reject a group request that lists the same queue twice. When that rejection does not happen, ksft_raises only records a failed check and leaves cm.exception as None, so the following errno check raises AttributeError. Worse, the accepted group request leaves a node shaper and queue 0 behind, which makes later tests fail for an unrelated reason. Handle the negative test explicitly instead. If group fails, verify that the errno is EINVAL and return. If group succeeds, delete the node returned by the operation and queue 0 before reporting the failure. Give the duplicate leaves different weights so the request still contains two distinct leaf entries while exercising duplicate handle validation. This also introduces _delete_shaper(), cached _cap_get(), and _require_caps() helpers as preparation for the following shaper group tests. The follow-on tests need the same capability checks for node and queue scope support. Keeping that logic in one place avoids repeating raw EOPNOTSUPP handling in each test. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-3-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 145 +++++++++++------- 1 file changed, 86 insertions(+), 59 deletions(-) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index c80a4bf8cc05..1954f3263f25 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -2,13 +2,52 @@ # SPDX-License-Identifier: GPL-2.0 import errno +import glob from lib.py import ksft_run, ksft_exit -from lib.py import ksft_eq, ksft_raises, ksft_true, KsftSkipEx +from lib.py import ksft_eq, ksft_true, ksft_raises, KsftSkipEx from lib.py import EthtoolFamily, NetshaperFamily from lib.py import NetDrvEnv from lib.py import NlError -from lib.py import cmd +from lib.py import cmd, defer + +def _delete_shaper(cfg, nl_shaper, handle) -> None: + """ Delete the shaper identified by handle, ignoring a missing-shaper error. """ + try: + nl_shaper.delete({'ifindex': cfg.ifindex, + 'handle': handle}) + except NlError as e: + if e.error != errno.ENOENT: + raise + +def _require_queues(cfg, count): + """ Return the netdev TX queue count, skipping the test if fewer than count exist. """ + qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/tx-*")) + if qcnt < count: + raise KsftSkipEx(f"netdev has {qcnt} queues, {count} required") + return qcnt + +def _cap_get(cfg, nl_shaper, scope): + """ Return the shaper capabilities for the given scope, caching them on cfg. """ + if not hasattr(cfg, 'cap_cache'): + cfg.cap_cache = {} + if scope not in cfg.cap_cache: + cfg.cap_cache[scope] = nl_shaper.cap_get({'ifindex': cfg.ifindex, + 'scope': scope}) + + return cfg.cap_cache[scope] + +def _require_caps(cfg, nl_shaper, scope, caps, msg) -> None: + """ Skip the test unless the given scope advertises all the required caps. """ + try: + supported = _cap_get(cfg, nl_shaper, scope) + except NlError as e: + if e.error == errno.EOPNOTSUPP: + raise KsftSkipEx(f"{scope} scope shapers not supported by the device") + raise + + if not set(caps).issubset(supported): + raise KsftSkipEx(msg) def get_shapers(cfg, nl_shaper) -> None: try: @@ -44,17 +83,8 @@ def set_qshapers(cfg, nl_shaper) -> None: if not 'support-bw-max' in caps or not 'support-metric-bps' in caps: raise KsftSkipEx("device does not support queue scope shapers with bw_max and metric bps") - cfg.queues = True; - netnl = EthtoolFamily() - channels = netnl.channels_get({'header': {'dev-index': cfg.ifindex}}) - if channels['combined-count'] == 0: - cfg.rx_type = 'rx' - cfg.nr_queues = channels['rx-count'] - else: - cfg.rx_type = 'combined' - cfg.nr_queues = channels['combined-count'] - if cfg.nr_queues < 3: - raise KsftSkipEx(f"device does not support enough queues min 3 found {cfg.nr_queues}") + _require_queues(cfg, 3) + cfg.queues = True nl_shaper.set({'ifindex': cfg.ifindex, 'handle': {'scope': 'queue', 'id': 1}, @@ -140,8 +170,7 @@ def del_nshapers(cfg, nl_shaper) -> None: def basic_groups(cfg, nl_shaper) -> None: if not cfg.netdev: raise KsftSkipEx("netdev shaper not supported by the device") - if cfg.nr_queues < 3: - raise KsftSkipEx(f"netdev does not have enough queues min 3 reported {cfg.nr_queues}") + _require_queues(cfg, 3) try: caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, @@ -186,28 +215,14 @@ def basic_groups(cfg, nl_shaper) -> None: 'handle': {'scope': 'netdev'}}) def qgroups(cfg, nl_shaper) -> None: - if cfg.nr_queues < 4: - raise KsftSkipEx(f"netdev does not have enough queues min 4 reported {cfg.nr_queues}") - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'node'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("shapers not supported by the device") - raise - if not 'support-bw-max' in caps or not 'support-metric-bps' in caps: - raise KsftSkipEx("device does not support node scope shapers with bw_max and metric bps") - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'queue'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("shapers not supported by the device") - raise - if not 'support-nesting' in caps or not 'support-weight' in caps or not 'support-metric-bps' in caps: - raise KsftSkipEx("device does not support nested queue scope shapers with weight") + _require_queues(cfg, 4) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', + ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") - cfg.groups = True; node_handle = nl_shaper.group({ 'ifindex': cfg.ifindex, 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, @@ -285,17 +300,12 @@ def qgroups(cfg, nl_shaper) -> None: ksft_eq(len(shapers), 0) def delegation(cfg, nl_shaper) -> None: - if not cfg.groups: - raise KsftSkipEx("device does not support node scope") - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'node'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("node scope shapers not supported by the device") - raise - if not 'support-nesting' in caps: - raise KsftSkipEx("device does not support node scope shapers nesting") + _require_queues(cfg, 4) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") node_handle = nl_shaper.group({ 'ifindex': cfg.ifindex, @@ -376,19 +386,24 @@ def delegation(cfg, nl_shaper) -> None: ksft_eq(len(shapers), 0) def queue_update(cfg, nl_shaper) -> None: - if cfg.nr_queues < 4: - raise KsftSkipEx(f"netdev does not have enough queues min 4 reported {cfg.nr_queues}") + nq = _require_queues(cfg, 4) if not cfg.queues: raise KsftSkipEx("device does not support queue scope") + netnl = EthtoolFamily() + channels = netnl.channels_get({'header': {'dev-index': cfg.ifindex}}) + ch_type = 'combined' if channels['combined-count'] else 'tx' + for i in range(3): nl_shaper.set({'ifindex': cfg.ifindex, 'handle': {'scope': 'queue', 'id': i}, 'metric': 'bps', 'bw-max': (i + 1) * 1000}) + defer(cmd, f"ethtool -L {cfg.dev['ifname']} {ch_type} {nq}") + # Delete a channel, with no shapers configured on top of the related # queue: no changes expected - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} 3") + cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} 3") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, @@ -408,7 +423,7 @@ def queue_update(cfg, nl_shaper) -> None: # Delete a channel, with a shaper configured on top of the related # queue: the shaper must be deleted, too - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} 2") + cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} 2") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, @@ -423,7 +438,7 @@ def queue_update(cfg, nl_shaper) -> None: 'bw-max': 2000}]) # Restore the original channels number, no expected changes - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} {cfg.nr_queues}") + cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} {nq}") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, @@ -443,25 +458,37 @@ def queue_update(cfg, nl_shaper) -> None: def dup_leaves(cfg, nl_shaper) -> None: """ Ensure that the kernel rejects duplicate leaves. """ - if not cfg.groups: - raise KsftSkipEx("device does not support node scope") + _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + node_handle = None with ksft_raises(NlError) as cm: - nl_shaper.group({ + node_handle = nl_shaper.group({ 'ifindex': cfg.ifindex, - 'leaves':[{'handle': {'scope': 'queue', 'id': 0}}, - {'handle': {'scope': 'queue', 'id': 0}}], + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 0}, + 'weight': 2}], 'handle': {'scope':'node'}, 'metric': 'bps', 'bw-max': 10000}) + + # Clean up in case the kernel wrongly accepted the request. + if node_handle: + _delete_shaper(cfg, nl_shaper, node_handle['handle']) + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + # ksft_raises() has already recorded the failure if nothing was raised. + if cm.exception is None: + return ksft_eq(cm.exception.error, errno.EINVAL) def main() -> None: with NetDrvEnv(__file__, queue_count=4) as cfg: cfg.queues = False cfg.netdev = False - cfg.groups = False - cfg.nr_queues = 0 ksft_run([get_shapers, get_caps, set_qshapers, From 1b5c2eb00e596002c9414ca2cf90c51053159f00 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:25 -0700 Subject: [PATCH 03/14] selftests: net: shaper: Decouple basic_groups from netdev rate limiting Decouple basic_groups from the set_nshapers test dependency. The test was gated on cfg.netdev which is set by set_nshapers. Replace with direct capability checks: netdev scope support (required for grouping under netdev handle) and queue scope nesting + weight. Remove bw-max and metric from the .group call so the test validates pure queue grouping without rate limiting. The rate-limited variant is restored in the following patch, which adds a dedicated basic_groups_with_rate test. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-4-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 1954f3263f25..45a4bf42995e 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -168,19 +168,11 @@ def del_nshapers(cfg, nl_shaper) -> None: ksft_eq(len(shapers), 0) def basic_groups(cfg, nl_shaper) -> None: - if not cfg.netdev: - raise KsftSkipEx("netdev shaper not supported by the device") _require_queues(cfg, 3) - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'queue'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("shapers not supported by the device") - raise - if not 'support-weight' in caps: - raise KsftSkipEx("device does not support queue scope shapers with weight") + _require_caps(cfg, nl_shaper, 'netdev', [], "netdev scope not supported by the device") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "queue scope not supported with nesting and weight") node_handle = nl_shaper.group({ 'ifindex': cfg.ifindex, @@ -188,31 +180,31 @@ def basic_groups(cfg, nl_shaper) -> None: 'weight': 1}, {'handle': {'scope': 'queue', 'id': 2}, 'weight': 2}], - 'handle': {'scope':'netdev'}, - 'metric': 'bps', - 'bw-max': 10000}) + 'handle': {'scope':'netdev'}}) ksft_eq(node_handle, {'ifindex': cfg.ifindex, 'handle': {'scope': 'netdev'}}) + del_node = defer(_delete_shaper, cfg, nl_shaper, {'scope': 'netdev'}) + del_queues = [defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'queue', 'id': qid}) + for qid in (1, 2)] + shaper = nl_shaper.get({'ifindex': cfg.ifindex, 'handle': {'scope': 'queue', 'id': 1}}) ksft_eq(shaper, {'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, 'handle': {'scope': 'queue', 'id': 1}, 'weight': 1 }) + for dq in del_queues: + dq.exec() - nl_shaper.delete({'ifindex': cfg.ifindex, - 'handle': {'scope': 'queue', 'id': 2}}) - nl_shaper.delete({'ifindex': cfg.ifindex, - 'handle': {'scope': 'queue', 'id': 1}}) - - # Deleting all the leaves shaper does not affect the node one - # when the latter has 'netdev' scope. shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) - ksft_eq(len(shapers), 1) + ksft_eq(shapers, [{'ifindex': cfg.ifindex, + 'handle': {'scope': 'netdev'}}]) - nl_shaper.delete({'ifindex': cfg.ifindex, - 'handle': {'scope': 'netdev'}}) + del_node.exec() + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) def qgroups(cfg, nl_shaper) -> None: _require_queues(cfg, 4) From ff0c37b8c134ace868ea34a0560a9187edd10704 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:26 -0700 Subject: [PATCH 04/14] selftests: net: shaper: Add basic_groups_with_rate test Add a test that groups queues under the netdev parent with rate limiting enabled. Extract the common group-under-netdev flow into _group_under_netdev helper to share with basic_groups. The test independently checks for netdev scope bw_max and metric capabilities before proceeding, and verifies that the netdev shaper persists after leaf deletion. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-5-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 79 ++++++++++++++++--- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 45a4bf42995e..168a8dd057e5 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -167,20 +167,25 @@ def del_nshapers(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) -def basic_groups(cfg, nl_shaper) -> None: - _require_queues(cfg, 3) +def _group_under_netdev(cfg, nl_shaper, bw_max=None): + r"""Group queues under a netdev-scope node; caller owns node teardown. - _require_caps(cfg, nl_shaper, 'netdev', [], "netdev scope not supported by the device") - _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], - "queue scope not supported with nesting and weight") + netdev netdev + / \ del Q1,Q2 + Q1 Q2 -------> (netdev node persists) + """ + group_args = { + 'ifindex': cfg.ifindex, + 'leaves': [{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 2}], + 'handle': {'scope': 'netdev'}} + if bw_max: + group_args['metric'] = 'bps' + group_args['bw-max'] = bw_max - node_handle = nl_shaper.group({ - 'ifindex': cfg.ifindex, - 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, - 'weight': 1}, - {'handle': {'scope': 'queue', 'id': 2}, - 'weight': 2}], - 'handle': {'scope':'netdev'}}) + node_handle = nl_shaper.group(group_args) ksft_eq(node_handle, {'ifindex': cfg.ifindex, 'handle': {'scope': 'netdev'}}) @@ -194,10 +199,29 @@ def basic_groups(cfg, nl_shaper) -> None: ksft_eq(shaper, {'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, 'handle': {'scope': 'queue', 'id': 1}, - 'weight': 1 }) + 'weight': 1}) for dq in del_queues: dq.exec() + # Caller owns the node teardown so it can verify the netdev-scope node + # survives leaf deletion before removing it. + return del_node + +def basic_groups(cfg, nl_shaper) -> None: + r"""Group queues under a netdev-scope node, then tear it down. + + netdev + / \ + Q1 Q2 + """ + _require_queues(cfg, 3) + + _require_caps(cfg, nl_shaper, 'netdev', [], "netdev scope not supported by the device") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "queue scope not supported with nesting and weight") + + del_node = _group_under_netdev(cfg, nl_shaper) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, 'handle': {'scope': 'netdev'}}]) @@ -206,6 +230,34 @@ def basic_groups(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def basic_groups_with_rate(cfg, nl_shaper) -> None: + r"""Rate-limited netdev-scope node outlives deletion of its leaves. + + netdev[10kbps] netdev[10kbps] + / \ del Q1,Q2 + Q1 Q2 -------> (node persists) + """ + bw_max = 10000 + + _require_queues(cfg, 3) + + _require_caps(cfg, nl_shaper, 'netdev', ['support-bw-max', 'support-metric-bps'], + "device does not support netdev scope rate limiting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support queue scope shapers with nesting and weight") + + del_node = _group_under_netdev(cfg, nl_shaper, bw_max=bw_max) + + # Deleting all the leaves shaper does not affect the node one + # when the latter has 'netdev' scope. + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(shapers, [{'ifindex': cfg.ifindex, + 'handle': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': bw_max}]) + + del_node.exec() + def qgroups(cfg, nl_shaper) -> None: _require_queues(cfg, 4) _require_caps(cfg, nl_shaper, 'node', @@ -488,6 +540,7 @@ def main() -> None: set_nshapers, del_nshapers, basic_groups, + basic_groups_with_rate, qgroups, delegation, dup_leaves, From 212410dc81df028a99064b3415b2c1f0af5018de Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:27 -0700 Subject: [PATCH 05/14] selftests: net: shaper: Add node scope .set rate update test Add set_node_shaper to test updating a NODE scope shaper's rate via the .set callback. Creates a node group with bw_max=10000, updates to 20000 via .set, and verifies the change. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-6-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 168a8dd057e5..62ac83b7701c 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -343,6 +343,43 @@ def qgroups(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def set_node_shaper(cfg, nl_shaper) -> None: + """ Verify a node-scope shaper rate can be updated via .set. """ + _require_queues(cfg, 2) + _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + node_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + node_id = node_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + + # Update the node's rate via .set + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'metric': 'bps', + 'bw-max': 20000}) + + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': 20000}) + + # Cleanup + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def delegation(cfg, nl_shaper) -> None: _require_queues(cfg, 4) _require_caps(cfg, nl_shaper, 'node', @@ -542,6 +579,7 @@ def main() -> None: basic_groups, basic_groups_with_rate, qgroups, + set_node_shaper, delegation, dup_leaves, queue_update], From 047735744dedecde78d06a50ed74fb76ad739556 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:28 -0700 Subject: [PATCH 06/14] selftests: net: shaper: Add .group rate update test Add group_update_rate to test updating an existing node's rate via the .group callback. Creates a node with bw_max=10000, re-groups with bw_max=50000, and verifies the rate changed while leaves remain under the same node. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-7-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 62ac83b7701c..5eccbe437ba3 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -380,6 +380,72 @@ def set_node_shaper(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def group_update_rate(cfg, nl_shaper) -> None: + """ Verify re-grouping a node updates its rate while leaving the leaves untouched. """ + _require_queues(cfg, 3) + _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + # Create node with Q1, Q2 at bw_max=10000 + node_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + node_id = node_handle['handle']['id'] + for i in range(1, 3): + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) + + # Update rate via .group on the same node + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': node_id}, + 'metric': 'bps', + 'bw-max': 50000}) + + # Verify rate updated + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': 50000}) + + # Verify leaves unchanged + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}) + shaper_q2 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 2}}) + ksft_eq(shaper_q2, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}) + + # Make sure we only have 3 shapers including 2 queues and the node + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 3) + + # Cleanup + for i in range(1, 3): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def delegation(cfg, nl_shaper) -> None: _require_queues(cfg, 4) _require_caps(cfg, nl_shaper, 'node', @@ -580,6 +646,7 @@ def main() -> None: basic_groups_with_rate, qgroups, set_node_shaper, + group_update_rate, delegation, dup_leaves, queue_update], From 1e89d0d7430c42b2eb819fceaa284b705d5cf783 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:29 -0700 Subject: [PATCH 07/14] selftests: net: shaper: Add nested depth limit discovery test Add nested_depth_limit to incrementally create deeper nesting levels until the driver rejects. Reports the maximum supported nesting depth on both pass and fail. A device advertising nesting support must support at least depth 2, otherwise nesting is meaningless. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-8-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 5eccbe437ba3..3b72661202f9 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -532,6 +532,122 @@ def delegation(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def nested_depth_limit(cfg, nl_shaper) -> None: + r"""Nest nodes as deep as the device allows to find the max depth. + + netdev + | + N1 -- Q1 + | + N2 -- Q2 + | + N3 -- Q3 + : (deepen until the driver rejects) + """ + bw_max = 10000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + nq = _require_queues(cfg, 3) + + node_ids = [] + cleanups = [] + queue_id = 1 + max_depth = 0 + limit_err = None + + # Create initial node with a queue leaf + node_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves': [{'handle': {'scope': 'queue', 'id': queue_id}, + 'weight': 1}], + 'handle': {'scope': 'node'}, + 'metric': 'bps', + 'bw-max': bw_max})['handle']['id'] + node_ids.append(node_id) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'node', 'id': node_id})) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'queue', 'id': queue_id})) + max_depth = 1 + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': queue_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': queue_id}, + 'weight': 1}) + queue_id += 1 + + # Keep nesting deeper until the driver rejects or queues run out. + while queue_id < nq: + parent_id = node_ids[-1] + try: + node_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves': [{'handle': {'scope': 'queue', + 'id': queue_id}, + 'weight': 1}], + 'handle': {'scope': 'node'}, + 'parent': {'scope': 'node', + 'id': parent_id}, + 'metric': 'bps', + 'bw-max': bw_max})['handle']['id'] + except NlError as e: + # Only treat "cannot nest deeper" errors as the depth limit; + # drivers report it differently (EOPNOTSUPP/ENOSPC/E2BIG/EINVAL). + # Anything else (ENOMEM, EIO, EPERM, driver bug) is a real failure. + if e.error not in (errno.EOPNOTSUPP, errno.ENOSPC, + errno.E2BIG, errno.EINVAL): + raise + limit_err = e + break + + node_ids.append(node_id) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'node', 'id': node_id})) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'queue', 'id': queue_id})) + max_depth += 1 + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'node', 'id': parent_id}, + 'metric': 'bps', + 'bw-max': bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', + 'id': queue_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': queue_id}, + 'weight': 1}) + queue_id += 1 + + if limit_err: + print(f"# max nesting depth supported: {max_depth} (errno {limit_err.error})") + else: + print(f"# max nesting depth tested: {max_depth}") + ksft_true(max_depth >= 2, + f"max nesting depth: {max_depth}") + + # Cleanup: exec the deferred deletes in reverse creation order, so each + # queue leaf and deeper node is removed before its parent node. + for cleanup in reversed(cleanups): + cleanup.exec() + ksft_eq(len(nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)), 0) + def queue_update(cfg, nl_shaper) -> None: nq = _require_queues(cfg, 4) if not cfg.queues: @@ -648,6 +764,7 @@ def main() -> None: set_node_shaper, group_update_rate, delegation, + nested_depth_limit, dup_leaves, queue_update], args=(cfg, NetshaperFamily())) From 5c84926ef73c855ebcf8d4cee26dcf5844012ca1 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:30 -0700 Subject: [PATCH 08/14] selftests: net: shaper: Add child node deletion reparent test Add delete_child_reparent to verify that deleting a child node reparents its queue leaves to the parent node. Creates a two-level hierarchy (N1 with Q1,Q2 and child N2 with Q3), deletes N2, and verifies Q3's parent becomes N1. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-9-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 3b72661202f9..1b88e183f1b9 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -648,6 +648,82 @@ def nested_depth_limit(cfg, nl_shaper) -> None: cleanup.exec() ksft_eq(len(nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)), 0) +def delete_child_reparent(cfg, nl_shaper) -> None: + r"""Deleting a child node reparents its queue leaf to the parent. + + netdev netdev + | | + N1 del N2 N1 + / | \ -----> / | \ + Q1 Q2 N2 Q1 Q2 Q3 + | + Q3 + """ + n1_bw_max = 10000 + n2_bw_max = 5000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + _require_queues(cfg, 4) + + # Create parent node N1 with Q1, Q2 + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + n1_id = n1_handle['handle']['id'] + for i in range(1, 3): + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) + + # Create child node N2 under N1 with Q3 + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'parent': {'scope': 'node', 'id': n1_id}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + n2_id = n2_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) + + # Delete child N2 - Q3 should reparent to N1 + nl_shaper.delete({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}}) + + with ksft_raises(NlError): + nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}}) + + shaper_n1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}}) + ksft_eq(shaper_n1, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + shaper_q3 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 3}}) + ksft_eq(shaper_q3, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n1_id}, + 'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}) + + # Cleanup + for i in range(1, 4): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def queue_update(cfg, nl_shaper) -> None: nq = _require_queues(cfg, 4) if not cfg.queues: @@ -765,6 +841,7 @@ def main() -> None: group_update_rate, delegation, nested_depth_limit, + delete_child_reparent, dup_leaves, queue_update], args=(cfg, NetshaperFamily())) From aa05d8096a9bc45dc0fbeca29dd41d25c3b1dbc0 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:31 -0700 Subject: [PATCH 09/14] selftests: net: shaper: Add queue migration between nodes test Add move_queue_between_nodes to verify that a queue can be moved from one node to another via re-grouping. Creates N1 with Q1,Q2 and N2 with Q3, then re-groups N2 with Q1,Q3 to steal Q1 from N1. Verifies Q1 moved to N2 and Q2 remains under N1. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-10-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 1b88e183f1b9..62c74a0c0563 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -724,6 +724,107 @@ def delete_child_reparent(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def move_queue_between_nodes(cfg, nl_shaper) -> None: + r"""Move a queue between nodes by re-grouping the destination node. + + netdev netdev + / \ .group N2 / \ + N1 N2 {Q1,Q3} N1 N2 + / \ | -------> | / \ + Q1 Q2 Q3 Q2 Q1 Q3 + """ + n1_bw_max = 10000 + n2_bw_max = 20000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + _require_queues(cfg, 4) + + # Create N1 with Q1, Q2 + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + n1_id = n1_handle['handle']['id'] + for i in range(1, 3): + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) + + # Create N2 with Q3 + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + n2_id = n2_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) + + # Move Q1 from N1 to N2 by re-grouping N2 with Q1, Q3 + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}, + {'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': n2_id}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + + shaper_n1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}}) + ksft_eq(shaper_n1, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + shaper_n2 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}}) + ksft_eq(shaper_n2, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + + # Verify Q1 moved to N2 + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}) + + # Verify Q2 still under N1 + shaper_q2 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 2}}) + ksft_eq(shaper_q2, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n1_id}, + 'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}) + + # Verify Q3 remained under N2 + shaper_q3 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 3}}) + ksft_eq(shaper_q3, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}) + + # Cleanup + for i in range(1, 4): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def queue_update(cfg, nl_shaper) -> None: nq = _require_queues(cfg, 4) if not cfg.queues: @@ -842,6 +943,7 @@ def main() -> None: delegation, nested_depth_limit, delete_child_reparent, + move_queue_between_nodes, dup_leaves, queue_update], args=(cfg, NetshaperFamily())) From 4f197c44981f829243236c99dc2c7b94874daba1 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:32 -0700 Subject: [PATCH 10/14] selftests: net: shaper: Add reparenting rejection test Add reject_reparenting to verify that the group operation rejects attempts to change an existing node's parent. The test creates two node shapers under netdev and verifies that re-grouping the first node under the second fails with EOPNOTSUPP. It also verifies that updating the node with the same parent succeeds, and that updating the node without specifying a parent keeps the queue leaves under the original node while updating their weights. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-11-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 62c74a0c0563..e7af94264409 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 +# pylint: disable=too-many-lines import errno import glob @@ -825,6 +826,154 @@ def move_queue_between_nodes(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def reject_reparenting(cfg, nl_shaper) -> None: + r"""Reject reparenting an existing node; the hierarchy stays intact. + + netdev + / \ rejected: N3 -> netdev + N1 N2 rejected: N1 -> N2 + / \ | (both EOPNOTSUPP) + Q1 N3 Q2 + | + Q3 + """ + node1_bw_max = 10000 + node2_bw_max = 5000 + node3_bw_max = 20000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + _require_queues(cfg, 4) + + # Create Node1 under netdev with Q1. + node1_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': node1_bw_max})['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node1_id}) + + # Create Node2 under netdev with Q2. + node2_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': node2_bw_max})['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 2}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node2_id}) + + # Create Node3 nested under Node1 with Q3. + node3_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': node3_bw_max, + 'parent': {'scope': 'node', 'id': node1_id}})['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node3_id}) + + # Reparenting a nested node up to netdev must fail. + with ksft_raises(NlError) as cm: + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': node3_id}, + 'parent': {'scope': 'netdev'}}) + if cm.exception: + ksft_eq(cm.exception.error, errno.EOPNOTSUPP) + + # Reparenting a node under another node must fail as well. + with ksft_raises(NlError) as cm: + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': node1_id}, + 'parent': {'scope': 'node', 'id': node2_id}}) + if cm.exception: + ksft_eq(cm.exception.error, errno.EOPNOTSUPP) + + # Updating a node with the same parent must succeed. + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 5}], + 'handle': {'scope':'node', 'id': node1_id}, + 'parent': {'scope': 'netdev'}}) + + # Updating a node without specifying the parent must succeed. + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 2}, + 'weight': 7}], + 'handle': {'scope':'node', 'id': node2_id}}) + + # The rejected reparents must have left the hierarchy intact. + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node1_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node1_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': node1_bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node2_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node2_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': node2_bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node3_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node3_id}, + 'parent': {'scope': 'node', 'id': node1_id}, + 'metric': 'bps', + 'bw-max': node3_bw_max}) + + # Verify the leaf weights were updated and parents unchanged. + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node1_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 5}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 2}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node2_id}, + 'handle': {'scope': 'queue', 'id': 2}, + 'weight': 7}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 3}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node3_id}, + 'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}) + + # Cleanup. Delete the nodes explicitly instead of relying on the + # empty-node auto-delete: a kernel that wrongly accepts a reparent may + # mishandle the leaf accounting and leave a node behind. Removing them + # by handle keeps a failing run from leaking state into later tests. + for i in range(1, 4): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + for nid in (node1_id, node2_id, node3_id): + _delete_shaper(cfg, nl_shaper, {'scope': 'node', 'id': nid}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def queue_update(cfg, nl_shaper) -> None: nq = _require_queues(cfg, 4) if not cfg.queues: @@ -944,6 +1093,7 @@ def main() -> None: nested_depth_limit, delete_child_reparent, move_queue_between_nodes, + reject_reparenting, dup_leaves, queue_update], args=(cfg, NetshaperFamily())) From 83731be09057349559f588f11dcf4619198c018b Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:33 -0700 Subject: [PATCH 11/14] selftests: net: shaper: Cover scalar attributes Exercise queue-scope scalar shaper attributes reported by the device, including rate limits, burst, priority and weight. Build the set request from advertised capabilities so devices are tested for the attributes they claim rather than skipped for missing unrelated fields. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-12-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index e7af94264409..8dd4897e999e 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -168,6 +168,73 @@ def del_nshapers(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def set_all_supported_attrs(cfg, nl_shaper) -> None: + """ Set every queue-scope attribute the device advertises and verify the read-back. """ + _require_queues(cfg, 1) + + _require_caps(cfg, nl_shaper, 'queue', [], + "queue scope shapers not supported by the device") + caps = _cap_get(cfg, nl_shaper, 'queue') + + attrs = {'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}} + expected = {'ifindex': cfg.ifindex, + 'parent': {'scope': 'netdev'}, + 'handle': {'scope': 'queue', 'id': 0}} + + rate_attrs = {'support-bw-min': ('bw-min', 10000, 100), + 'support-bw-max': ('bw-max', 20000, 200), + 'support-burst': ('burst', 3000, 30)} + rate_attr_supported = any(cap in caps for cap in rate_attrs) + bps_supported = 'support-metric-bps' in caps + pps_supported = 'support-metric-pps' in caps + + def add_rate_attrs(metric, value_idx) -> None: + attrs['metric'] = metric + expected['metric'] = metric + for cap, (attr, bps_value, pps_value) in rate_attrs.items(): + if cap not in caps: + continue + + value = bps_value if value_idx == 0 else pps_value + attrs[attr] = value + expected[attr] = value + + if rate_attr_supported: + if bps_supported: + add_rate_attrs('bps', 0) + elif pps_supported: + add_rate_attrs('pps', 1) + + if 'support-priority' in caps: + attrs['priority'] = 1 + expected['priority'] = 1 + if 'support-weight' in caps: + attrs['weight'] = 2 + expected['weight'] = 2 + + if len(attrs) == 2: + raise KsftSkipEx("device does not advertise any supported queue shaper attributes") + + nl_shaper.set(attrs) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, expected) + + if rate_attr_supported and bps_supported and pps_supported: + add_rate_attrs('pps', 1) + nl_shaper.set(attrs) + + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, expected) + + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def _group_under_netdev(cfg, nl_shaper, bw_max=None): r"""Group queues under a netdev-scope node; caller owns node teardown. @@ -1084,6 +1151,7 @@ def main() -> None: del_qshapers, set_nshapers, del_nshapers, + set_all_supported_attrs, basic_groups, basic_groups_with_rate, qgroups, From 3651c7e18c04d65cd467202b80628cdb445b58a6 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:34 -0700 Subject: [PATCH 12/14] selftests: net: shaper: Reject invalid set requests Verify that invalid set requests fail without corrupting existing queue shaper state. The test covers invalid node creation through set and invalid queue identifiers, then confirms the original queue configuration remains unchanged. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-13-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 8dd4897e999e..02a11e6b9a05 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -235,6 +235,46 @@ def set_all_supported_attrs(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def invalid_set_preserves_state(cfg, nl_shaper) -> None: + """ Verify a rejected .set leaves the existing shaper configuration unchanged. """ + nq = _require_queues(cfg, 1) + _require_caps(cfg, nl_shaper, 'queue', + ['support-bw-max', 'support-metric-bps'], + "device does not support queue scope bw_max with bps metric") + + initial = {'ifindex': cfg.ifindex, + 'parent': {'scope': 'netdev'}, + 'handle': {'scope': 'queue', 'id': 0}, + 'metric': 'bps', + 'bw-max': 10000} + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}, + 'metric': 'bps', + 'bw-max': 10000}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + with ksft_raises(NlError): + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': 0}, + 'metric': 'bps', + 'bw-max': 20000}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, initial) + + with ksft_raises(NlError): + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': nq}, + 'metric': 'bps', + 'bw-max': 20000}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, initial) + + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def _group_under_netdev(cfg, nl_shaper, bw_max=None): r"""Group queues under a netdev-scope node; caller owns node teardown. @@ -1152,6 +1192,7 @@ def main() -> None: set_nshapers, del_nshapers, set_all_supported_attrs, + invalid_set_preserves_state, basic_groups, basic_groups_with_rate, qgroups, From ca157503b5b82e9aceef148c22f6ad7c24aff466 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:35 -0700 Subject: [PATCH 13/14] selftests: net: shaper: Cover mixed-parent grouping Add coverage for grouping leaves that currently belong to different parent nodes. The test verifies that an implicit parent is rejected, an explicit parent succeeds, and the old empty parent nodes are cleaned up. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-14-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 02a11e6b9a05..9264aeb74a7a 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -275,6 +275,105 @@ def invalid_set_preserves_state(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def mixed_parent_group_requires_parent(cfg, nl_shaper) -> None: + r"""Grouping leaves from different nodes requires an explicit parent. + + netdev netdev + / \ parent=netdev + N1 N2 group N + | | {Q0,Q1} / \ + Q0 Q1 -------> Q0 Q1 + + Without an explicit parent the group is rejected; parent=netdev + collapses the leaves into one new node. + """ + _require_queues(cfg, 2) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', + ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + n1_id = n1_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 20000}) + n2_id = n2_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + + with ksft_raises(NlError): + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 3}, + {'handle': {'scope': 'queue', 'id': 1}, + 'weight': 4}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 30000}) + + shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n1_id}, + 'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}) + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}) + + node_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 3}, + {'handle': {'scope': 'queue', 'id': 1}, + 'weight': 4}], + 'handle': {'scope':'node'}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': 30000}) + node_id = node_handle['handle']['id'] + + for old_id in (n1_id, n2_id): + with ksft_raises(NlError): + nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': old_id}}) + + shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 0}, + 'weight': 3}) + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 4}) + + for i in range(2): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def _group_under_netdev(cfg, nl_shaper, bw_max=None): r"""Group queues under a netdev-scope node; caller owns node teardown. @@ -1193,6 +1292,7 @@ def main() -> None: del_nshapers, set_all_supported_attrs, invalid_set_preserves_state, + mixed_parent_group_requires_parent, basic_groups, basic_groups_with_rate, qgroups, From e09d72c1c80d43f0785b4158864e90963915bb20 Mon Sep 17 00:00:00 2001 From: Mohsin Bashir Date: Tue, 4 Aug 2026 20:09:36 -0700 Subject: [PATCH 14/14] selftests: net: shaper: Cover recursive node cleanup Exercise cleanup of nested nodes after deleting their last queue leaf. The test builds a two-level node hierarchy and checks that removing the queue also removes both now-empty node shapers. Signed-off-by: Mohsin Bashir Link: https://patch.msgid.link/20260805030936.1092907-15-mohsin.bashr@gmail.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/shaper.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index 9264aeb74a7a..a53316726f69 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -374,6 +374,64 @@ def mixed_parent_group_requires_parent(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def recursive_empty_node_cleanup(cfg, nl_shaper) -> None: + r"""Deleting the last leaf recursively removes the emptied ancestors. + + netdev netdev + | del Q0 + N1 ------> (N1 and N2 removed too) + | + N2 + | + Q0 + """ + _require_queues(cfg, 1) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support nested node scope shapers") + _require_caps(cfg, nl_shaper, 'queue', + ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + n1_id = n1_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'parent': {'scope': 'node', 'id': n1_id}, + 'metric': 'bps', + 'bw-max': 5000}) + n2_id = n2_handle['handle']['id'] + + shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}) + + nl_shaper.delete({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + + for handle in ({'scope': 'queue', 'id': 0}, + {'scope': 'node', 'id': n2_id}, + {'scope': 'node', 'id': n1_id}): + with ksft_raises(NlError): + nl_shaper.get({'ifindex': cfg.ifindex, 'handle': handle}) + + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def _group_under_netdev(cfg, nl_shaper, bw_max=None): r"""Group queues under a netdev-scope node; caller owns node teardown. @@ -1293,6 +1351,7 @@ def main() -> None: set_all_supported_attrs, invalid_set_preserves_state, mixed_parent_group_requires_parent, + recursive_empty_node_cleanup, basic_groups, basic_groups_with_rate, qgroups,