Commit 7d5e5cbe04 for frr

commit 7d5e5cbe047d5d96ac40029aa66aa0794f88a1cc
Author: Srinivasan Koona Lokabiraman <srinivasan@nexthop.ai>
Date:   Fri Oct 23 05:34:38 2026 +0000

    tests: check color extcommunity config output re-parses

    test_ecommunity: add color-format and color-format-co cases. Each
    parses a color extended community, checks the COMMUNITY_LIST,
    ROUTE_MAP and DISPLAY renderings ("color CC:VALUE" for the first two,
    "Color:CC:VALUE" for display), and parses the COMMUNITY_LIST output
    again to check it round-trips.

    bgp_color_extcommunities: add
    test_bgp_color_extcommunity_list_config_roundtrip, which configures
    color extcommunity-lists, takes their lines from running-config,
    enters them again and checks they are accepted and unchanged.

    Signed-off-by: Srinivasan Koona Lokabiraman <srinivasan@nexthop.ai>

diff --git a/tests/bgpd/test_ecommunity.c b/tests/bgpd/test_ecommunity.c
index 2f550a557b..e250dc3005 100644
--- a/tests/bgpd/test_ecommunity.c
+++ b/tests/bgpd/test_ecommunity.c
@@ -183,6 +183,89 @@ static void lb_display_test(struct lb_segment *t)
 }


+/* Color extended-community formatting.
+ *
+ * COMMUNITY_LIST and ROUTE_MAP render config that has to parse again, so both
+ * must emit "color CC:VALUE"; DISPLAY stays human-readable as "Color:CC:VALUE".
+ * The CO field is two binary digits, hence 11 == 3.
+ */
+static struct color_segment {
+	const char *name;
+	const char *str;     /* input, extcommunity-list syntax */
+	const char *reparse; /* expected COMMUNITY_LIST and ROUTE_MAP output */
+	const char *display; /* expected DISPLAY output */
+} color_segments[] = {
+	{"color-format", "color 00:100", "color 00:100", "Color:00:100"},
+	{"color-format-co", "color 11:55555", "color 11:55555",
+	 "Color:11:55555"},
+	{"color-format-max", "color 11:4294967295", "color 11:4294967295",
+	 "Color:11:4294967295"},
+	{NULL, NULL, NULL, NULL}};
+
+static int check_format(const char *what, struct ecommunity *ecom, int format,
+			const char *shouldbe)
+{
+	char *str = ecommunity_ecom2str(ecom, format, 0);
+	int fails = 0;
+
+	printf("  %-14s %s\n", what, str ? str : "NULL");
+	if (!str || strcmp(str, shouldbe)) {
+		printf("shouldbe: %s\n", shouldbe);
+		fails++;
+	}
+	XFREE(MTYPE_ECOMMUNITY_STR, str);
+
+	return fails;
+}
+
+static void color_test(struct color_segment *t)
+{
+	struct ecommunity *ecom, *etmp;
+	char *str1, *str2;
+	int fails = 0;
+
+	printf("%s: %s\n", t->name, t->str);
+
+	ecom = ecommunity_str2com(t->str, 0, 1);
+	if (!ecom) {
+		printf("shouldbe: %s\nparse returned NULL\n", t->reparse);
+		failed++;
+		printf("failed\n\n");
+		return;
+	}
+
+	fails += check_format("COMMUNITY_LIST", ecom,
+			      ECOMMUNITY_FORMAT_COMMUNITY_LIST, t->reparse);
+	fails += check_format("ROUTE_MAP", ecom, ECOMMUNITY_FORMAT_ROUTE_MAP,
+			      t->reparse);
+	fails += check_format("DISPLAY", ecom, ECOMMUNITY_FORMAT_DISPLAY,
+			      t->display);
+
+	/* the re-parseable form must survive a round trip */
+	str1 = ecommunity_ecom2str(ecom, ECOMMUNITY_FORMAT_COMMUNITY_LIST, 0);
+	etmp = str1 ? ecommunity_str2com(str1, 0, 1) : NULL;
+	if (etmp)
+		str2 = ecommunity_ecom2str(etmp,
+					   ECOMMUNITY_FORMAT_COMMUNITY_LIST, 0);
+	else
+		str2 = NULL;
+
+	if (!str2 || strcmp(str1, str2)) {
+		printf("dogfood: in %s\n"
+		       "    in->out %s\n",
+		       str1 ? str1 : "NULL", str2 ? str2 : "NULL");
+		fails++;
+	}
+
+	ecommunity_free(&etmp);
+	ecommunity_free(&ecom);
+	XFREE(MTYPE_ECOMMUNITY_STR, str1);
+	XFREE(MTYPE_ECOMMUNITY_STR, str2);
+
+	failed += fails;
+	printf(fails ? "failed\n\n" : "OK\n\n");
+}
+
 int main(void)
 {
 	int i = 0;
@@ -192,6 +275,10 @@ int main(void)
 	for (i = 0; lb_segments[i].name; i++)
 		lb_display_test(&lb_segments[i]);

+	i = 0;
+	while (color_segments[i].name)
+		color_test(&color_segments[i++]);
+
 	printf("failures: %d\n", failed);
 	// printf ("aspath count: %ld\n", aspath_count());
 	return failed;
diff --git a/tests/bgpd/test_ecommunity.py b/tests/bgpd/test_ecommunity.py
index 9080d42087..a13948b353 100644
--- a/tests/bgpd/test_ecommunity.py
+++ b/tests/bgpd/test_ecommunity.py
@@ -13,3 +13,6 @@ TestEcommunity.okfail("asn4")
 TestEcommunity.okfail("lb-gbps")
 TestEcommunity.okfail("lb-mbps")
 TestEcommunity.okfail("lb-ipv6-gbps")
+TestEcommunity.okfail("color-format")
+TestEcommunity.okfail("color-format-co")
+TestEcommunity.okfail("color-format-max")
diff --git a/tests/topotests/bgp_color_extcommunities/test_bgp_color_extcommunities.py b/tests/topotests/bgp_color_extcommunities/test_bgp_color_extcommunities.py
index 94a444d5c5..224dd6f6bd 100644
--- a/tests/topotests/bgp_color_extcommunities/test_bgp_color_extcommunities.py
+++ b/tests/topotests/bgp_color_extcommunities/test_bgp_color_extcommunities.py
@@ -121,6 +121,54 @@ def test_bgp_color_extended_communities():
     ), "10.10.10.0/24 ext community is correctly not installed, but SHOULD be"


+def test_bgp_color_extcommunity_list_config_roundtrip():
+    "running-config output for a color extcommunity-list must be re-enterable"
+    tgen = get_topogen()
+
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    r1 = tgen.gears["r1"]
+
+    r1.vtysh_cmd(
+        """
+        configure terminal
+         bgp extcommunity-list standard COLOR1 permit color 00:100
+         bgp extcommunity-list standard COLOR2 permit color 11:55555
+        """
+    )
+
+    def _generated_line(name):
+        for line in r1.vtysh_cmd("show running-config").splitlines():
+            if line.startswith("bgp extcommunity-list standard {} ".format(name)):
+                return line.strip()
+        return None
+
+    for name, value in (("COLOR1", "00:100"), ("COLOR2", "11:55555")):
+        line = _generated_line(name)
+        assert line is not None, "{} missing from running-config".format(name)
+        assert "permit color {}".format(value) in line, (
+            "{} rendered as {!r}, expected a re-parseable 'color {}'".format(
+                name, line, value
+            )
+        )
+
+    # Feed the generated lines back in under new names; they must parse.
+    for name in ("COLOR1", "COLOR2"):
+        line = _generated_line(name)
+        reentered = line.replace(
+            "standard {} ".format(name), "standard RT_{} ".format(name)
+        )
+        output = r1.vtysh_cmd("configure terminal\n{}".format(reentered))
+        assert "Malformed" not in output, "re-entering {!r} failed: {}".format(
+            reentered, output
+        )
+
+        assert _generated_line("RT_{}".format(name)) == reentered, (
+            "round-trip of {} is not idempotent".format(name)
+        )
+
+
 if __name__ == "__main__":
     args = ["-s"] + sys.argv[1:]
     sys.exit(pytest.main(args))