Commit 4f2afd22fb for frr

commit 4f2afd22fb0932cffbfa2a47e0b45a25e3d57859
Author: Robin Christ <r.christ@partimus.com>
Date:   Wed Oct 14 10:45:16 2026 +0200

    tests: pin multi-RT route-target lists on an L3VNI VRF

    Covers repeated entries within one list and "both" on a split
    import/export state, which the CLI has to get right once an RTLIST
    is applied as one batch.

    Signed-off-by: Robin Christ <r.christ@partimus.com>

diff --git a/tests/topotests/bgp_evpn_route_target_config/test_bgp_evpn_route_target_config.py b/tests/topotests/bgp_evpn_route_target_config/test_bgp_evpn_route_target_config.py
index b36190f125..a5604a8f67 100644
--- a/tests/topotests/bgp_evpn_route_target_config/test_bgp_evpn_route_target_config.py
+++ b/tests/topotests/bgp_evpn_route_target_config/test_bgp_evpn_route_target_config.py
@@ -19,6 +19,9 @@ Test the EVPN route-target configuration features:
 - manual route targets replacing the auto route target per direction
   (add-if-no-manual default), and the split/merge behavior of
   "route-target both <RT>" and its no-forms,
+- adding and removing several manual route targets with one RTLIST
+  command on an L3VNI VRF, including repeated and already configured
+  entries,
 - canonical ordering of the effective import route-target list,
 - cleanup of the wildcard auto import RT when the L3VNI is detached,
 - the deprecated "route-target <dir> auto"/"autort rfc8365-compatible"
@@ -1161,6 +1164,146 @@ router bgp 65002 vrf vrf-101
     )


+def test_l3vni_rtlist():
+    """
+    Add and remove several manual RTs with one RTLIST command on r2's
+    L3VNI VRF. Entries already configured, or repeated within the same
+    list, warn while the remaining entries still apply. Route import
+    and export follow the resulting RT set in both directions, and
+    "route-target both" with a split import/export state only adds or
+    removes the missing or present direction.
+    """
+    tgen = get_topogen()
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    r1 = tgen.gears["r1"]
+    r2 = tgen.gears["r2"]
+
+    # A list that does not match r1's export RT replaces the auto import
+    # RT. The repeated entry warns once.
+    out = r2.vtysh_cmd(
+        """
+configure terminal
+router bgp 65002 vrf vrf-101
+ address-family l2vpn evpn
+  route-target import 65001:998 65001:999 65001:999
+"""
+    )
+    assert (
+        out.count("RT specified already configured for this VRF: 65001:999") == 1
+    ), "r2: repeated RT in an import list did not warn exactly once"
+
+    test_func = partial(
+        _check_vni_rt_list, r2, 101, "importRts", ["65001:998", "65001:999"]
+    )
+    _expect(test_func, "r2: import RTs of the L3VNI not set from the RT list")
+    test_func = partial(_check_vrf_route_absent, r2, "10.0.101.1/32")
+    _expect(test_func, "r2: r1's type-5 route still imported without a matching RT")
+
+    # Mixed list: the already configured entry warns, the new one applies.
+    out = r2.vtysh_cmd(
+        """
+configure terminal
+router bgp 65002 vrf vrf-101
+ address-family l2vpn evpn
+  route-target import 65001:101 65001:998
+"""
+    )
+    assert (
+        "RT specified already configured for this VRF: 65001:998" in out
+    ), "r2: already configured RT in an import list did not warn"
+
+    test_func = partial(
+        _check_vni_rt_list,
+        r2,
+        101,
+        "importRts",
+        ["65001:101", "65001:998", "65001:999"],
+    )
+    _expect(test_func, "r2: new RT of a mixed import list not applied")
+    _expect(
+        _check_vrf_route_present(r2, "10.0.101.1/32"),
+        "r2: r1's type-5 route not imported via the added RT",
+    )
+
+    # Removing all of them in one list restores the auto import RT. The
+    # repeated entry no longer matches the configuration and warns once.
+    out = r2.vtysh_cmd(
+        """
+configure terminal
+router bgp 65002 vrf vrf-101
+ address-family l2vpn evpn
+  no route-target import 65001:101 65001:998 65001:999 65001:999
+"""
+    )
+    assert (
+        out.count("RT specified does not match configuration for this VRF: 65001:999")
+        == 1
+    ), "r2: repeated RT in a removal list did not warn exactly once"
+
+    test_func = partial(_check_vni_rt_list, r2, 101, "importRts", ["*:101"])
+    _expect(test_func, "r2: auto import RT not restored after removing the RT list")
+    _expect(
+        _check_vrf_route_present(r2, "10.0.101.1/32"),
+        "r2: r1's type-5 route not imported via the restored auto RT",
+    )
+
+    # "both" on top of a split state: 65002:7 only lacks the import
+    # direction, 65002:8 lacks both. No entry warns.
+    out = r2.vtysh_cmd(
+        """
+configure terminal
+router bgp 65002 vrf vrf-101
+ address-family l2vpn evpn
+  route-target export 65002:7
+  route-target both 65002:7 65002:8
+"""
+    )
+    assert "RT specified" not in out, "r2: 'route-target both' on a split state warned"
+
+    test_func = partial(
+        _check_vni_rt_list, r2, 101, "importRts", ["65002:7", "65002:8"]
+    )
+    _expect(test_func, "r2: import RTs of the L3VNI not set by 'route-target both'")
+    test_func = partial(
+        _check_vni_rt_list, r2, 101, "exportRts", ["65002:7", "65002:8"]
+    )
+    _expect(test_func, "r2: export RTs of the L3VNI not set by 'route-target both'")
+    test_func = partial(_check_vrf_route_absent, r2, "10.0.101.1/32")
+    _expect(test_func, "r2: r1's type-5 route still imported without a matching RT")
+    test_func = partial(_check_vrf_route_absent, r1, "10.0.101.2/32")
+    _expect(test_func, "r1: r2's type-5 route still imported after r2's export change")
+
+    # Removing them restores both auto RTs and route exchange. The
+    # repeated entry warns once.
+    out = r2.vtysh_cmd(
+        """
+configure terminal
+router bgp 65002 vrf vrf-101
+ address-family l2vpn evpn
+  no route-target both 65002:7 65002:8 65002:8
+"""
+    )
+    assert (
+        out.count("RT specified does not match configuration for this VRF: 65002:8")
+        == 1
+    ), "r2: repeated RT in a 'no route-target both' list did not warn exactly once"
+
+    test_func = partial(_check_vni_rt_list, r2, 101, "importRts", ["*:101"])
+    _expect(test_func, "r2: auto import RT not restored after 'no route-target both'")
+    test_func = partial(_check_vni_rt_list, r2, 101, "exportRts", ["65002:101"])
+    _expect(test_func, "r2: auto export RT not restored after 'no route-target both'")
+    _expect(
+        _check_vrf_route_present(r2, "10.0.101.1/32"),
+        "r2: r1's type-5 route not re-imported after 'no route-target both'",
+    )
+    _expect(
+        _check_vrf_route_present(r1, "10.0.101.2/32"),
+        "r1: r2's type-5 route not re-imported after 'no route-target both'",
+    )
+
+
 def test_memory_leak():
     "Run the memory leak test and report results."
     tgen = get_topogen()