Commit 80eeca91ee for frr

commit 80eeca91ee0cc056022176afa42077e0aa504f39
Author: Vic Lan <vic.lan@pica8.com>
Date:   Thu Sep 17 22:04:48 2026 +0800

    tests: add NHG keep-around dependency refcount test

    Let the parent NHG enters KEEP_AROUND, then re-add the route before the
    keep timer expires and verify that each dependency refcount returns to
    its pre-delete value. This covers the case where a kept parent NHG is
    reused and must not recursively acquire an extra reference for each dependency.

    Signed-off-by: Vic Lan <vic.lan@pica8.com>

diff --git a/tests/topotests/zebra_nhg_keeparound_refcount/r1/frr.conf b/tests/topotests/zebra_nhg_keeparound_refcount/r1/frr.conf
new file mode 100644
index 0000000000..f41fdf4657
--- /dev/null
+++ b/tests/topotests/zebra_nhg_keeparound_refcount/r1/frr.conf
@@ -0,0 +1,6 @@
+!
+interface r1-eth0
+ ip address 192.168.1.1/24
+!
+line vty
+!
diff --git a/tests/topotests/zebra_nhg_keeparound_refcount/test_zebra_nhg_keeparound_refcount.py b/tests/topotests/zebra_nhg_keeparound_refcount/test_zebra_nhg_keeparound_refcount.py
new file mode 100644
index 0000000000..0290eeda77
--- /dev/null
+++ b/tests/topotests/zebra_nhg_keeparound_refcount/test_zebra_nhg_keeparound_refcount.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: ISC
+
+#
+# test_zebra_nhg_keeparound_refcount.py
+#
+# Copyright (c) 2026 by
+# Pica8, Inc.
+# Vic Lan
+#
+
+"""Verify NHG dependency references across keep-around reuse."""
+
+import json
+import os
+import sys
+
+import pytest
+
+CWD = os.path.dirname(os.path.realpath(__file__))
+sys.path.append(os.path.join(CWD, "../"))
+
+# pylint: disable=C0413
+from lib import topotest
+from lib.common_config import step
+from lib.topogen import Topogen, get_topogen
+from lib.topolog import logger
+
+
+def build_topo(tgen):
+    "Build function"
+    tgen.add_router("r1")
+
+    switch = tgen.add_switch("s1")
+    switch.add_link(tgen.gears["r1"])
+
+
+def setup_module(mod):
+    "Sets up the pytest environment"
+    tgen = Topogen(build_topo, mod.__name__)
+    tgen.start_topology()
+
+    for router in tgen.routers().values():
+        router.load_frr_config()
+
+    tgen.start_router()
+
+
+def teardown_module(_mod):
+    "Teardown the pytest environment"
+    tgen = get_topogen()
+    tgen.stop_topology()
+
+
+def test_keeparound_reuse_keeps_dependency_refs():
+    """Reusing a kept parent must not add dependency references."""
+    tgen = get_topogen()
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    r1 = tgen.gears["r1"]
+    prefix = "203.0.113.0/24"
+    nexthops = ["192.168.1.2", "192.168.1.3"]
+
+    def route_cmd(action):
+        r1.vtysh_cmd(
+            "configure terminal\n"
+            f" {action} ip route {prefix} {nexthops[0]}\n"
+            f" {action} ip route {prefix} {nexthops[1]}\n"
+            "end"
+        )
+
+    def nhg(nhg_id):
+        output = json.loads(r1.vtysh_cmd(f"show nexthop-group rib {nhg_id} json"))
+        return output.get(str(nhg_id))
+
+    try:
+        step("Install a static ECMP route")
+        route_cmd("")
+
+        def get_parent_id():
+            route = json.loads(r1.vtysh_cmd(f"show ip route {prefix} json"))
+            if prefix not in route:
+                return None
+            return route[prefix][0].get("receivedNexthopGroupId")
+
+        _, result = topotest.run_and_expect(
+            lambda: get_parent_id() is not None, True, count=30, wait=1
+        )
+        assert result, "Static ECMP route has no received NHG"
+        parent_id = get_parent_id()
+
+        parent = nhg(parent_id)
+        assert parent and len(parent.get("depends", [])) == 2
+        dependency_refs = {
+            dependency_id: nhg(dependency_id)["refCount"]
+            for dependency_id in parent["depends"]
+        }
+        logger.info(
+            "Parent NHG %s dependency refcounts before removal: %s",
+            parent_id,
+            dependency_refs,
+        )
+
+        step("Delete and re-add the route before the keep-around timer expires")
+        route_cmd("no")
+
+        def parent_kept():
+            parent = nhg(parent_id)
+            return parent and parent.get("keepAround") and parent["refCount"] == 1
+
+        _, result = topotest.run_and_expect(parent_kept, True, count=30, wait=1)
+        assert result, f"Parent NHG {parent_id} did not enter keep-around"
+
+        route_cmd("")
+
+        def parent_reused():
+            parent = nhg(parent_id)
+            return parent and not parent.get("keepAround")
+
+        _, result = topotest.run_and_expect(parent_reused, True, count=30, wait=1)
+        assert result, f"Parent NHG {parent_id} was not reused"
+
+        for dependency_id, refcount in dependency_refs.items():
+            current_refcount = nhg(dependency_id)["refCount"]
+            logger.info(
+                "Dependency NHG %s refcount after reuse: %s",
+                dependency_id,
+                current_refcount,
+            )
+            assert current_refcount == refcount
+    finally:
+        route_cmd("no")
+
+
+if __name__ == "__main__":
+    sys.exit(pytest.main(["-s"] + sys.argv[1:]))