Subject: [PATCH net-next 02/10] tools: ynl: convert netdev sample to selftest

From: Jakub Kicinski <[email-masked]>
Date: Thu, 5 Mar 2026 18:08:53 -0800

Convert netdev.c to produce KTAP output with 3 tests:
- dev_dump: dump all netdev devices, skip if empty
- dev_get: query first device from dump by ifindex
- ntf_check: subscribe to "mgmt", create a veth via rt-link,
  verify netdev notification is received, then delete the veth

Remove stdin/scanf-based UI. Add rt-link dependency for the veth
notification test.

  TAP version 13
  1..3
  # Starting 3 tests from 1 test cases.
  #  RUN           netdev.dump ...
  #       lo[1]	xdp-features (0): xdp-rx-metadata-features (0): xsk-fea...
  #     sit0[2]	xdp-features (0): xdp-rx-metadata-features (0): xsk-fea...
  #            OK  netdev.dump
  ok 1 netdev.dump
  #  RUN           netdev.get ...
  #       lo[1]	xdp-features (0): xdp-rx-metadata-features (0): xsk-fea...
  #            OK  netdev.get
  ok 2 netdev.get
  #  RUN           netdev.ntf_check ...
  #    veth0[7]	xdp-features (0): xdp-rx-metadata-features (7): timesta...
  #            OK  netdev.ntf_check
  ok 3 netdev.ntf_check
  # PASSED: 3 / 3 tests passed.
  # Totals: pass:3 fail:0 xfail:0 xpass:0 skip:0 error:0

Signed-off-by: Jakub Kicinski <[email-masked]>
---
 tools/net/ynl/tests/Makefile |  29 +++--
 tools/net/ynl/tests/netdev.c | 229 +++++++++++++++++++++++++----------
 2 files changed, 187 insertions(+), 71 deletions(-)

diff --git a/tools/net/ynl/tests/Makefile b/tools/net/ynl/tests/Makefile
index 5fa36c877235..1d77d3662f46 100644
--- a/tools/net/ynl/tests/Makefile
+++ b/tools/net/ynl/tests/Makefile
@@ -18,16 +18,29 @@ TEST_PROGS := \
 	test_ynl_ethtool.sh \
 # end of TEST_PROGS
 
-SRCS=$(wildcard *.c)
-BINS=$(patsubst %.c,%,${SRCS})
+TEST_GEN_PROGS := \
+	netdev \
+# end of TEST_GEN_PROGS
 
+BINS := \
+	devlink \
+	ethtool \
+	ovs \
+	rt-addr \
+	rt-link \
+	rt-route \
+	tc \
+	tc-filter-add \
+# end of BINS
+
+CFLAGS_netdev:=$(CFLAGS_netdev) $(CFLAGS_rt-link)
 CFLAGS_tc-filter-add:=$(CFLAGS_tc)
 
 include $(wildcard *.d)
 
 INSTALL_PATH ?= $(DESTDIR)/usr/share/kselftest
 
-all: $(BINS) $(TEST_PROGS)
+all: $(TEST_GEN_PROGS) $(BINS)
 
 ../lib/ynl.a:
 	@$(MAKE) -C ../lib
@@ -35,7 +48,7 @@ all: $(BINS) $(TEST_PROGS)
  ../generated/protos.a:
 	@$(MAKE) -C ../generated
 
-$(BINS): ../lib/ynl.a ../generated/protos.a
+$(TEST_GEN_PROGS) $(BINS): %: %.c ../lib/ynl.a ../generated/protos.a
 	@echo -e '\tCC test $@'
 	@$(COMPILE.c) $(CFLAGS_$@) $@.c -o $@.o
 	@$(LINK.c) $@.o -o $@  $(LDLIBS)
@@ -45,7 +58,7 @@ $(BINS): ../lib/ynl.a ../generated/protos.a
 		./$$test; \
 	done
 
-install: $(TEST_PROGS) $(BINS)
+install: $(TEST_GEN_PROGS) $(BINS)
 	@mkdir -p $(INSTALL_PATH)/ynl
 	@cp ../../../testing/selftests/kselftest/ktap_helpers.sh $(INSTALL_PATH)/
 	@for test in $(TEST_PROGS); do \
@@ -56,10 +69,10 @@ install: $(TEST_PROGS) $(BINS)
 		    $$test > $(INSTALL_PATH)/ynl/$$name; \
 		chmod +x $(INSTALL_PATH)/ynl/$$name; \
 	done
-	@for bin in $(BINS); do \
+	@for bin in $(TEST_GEN_PROGS) $(BINS); do \
 		cp $$bin $(INSTALL_PATH)/ynl/$$bin; \
 	done
-	@for test in $(TEST_PROGS); do \
+	@for test in $(TEST_PROGS) $(TEST_GEN_PROGS); do \
 		echo "ynl:$$test"; \
 	done > $(INSTALL_PATH)/kselftest-list.txt
 
@@ -67,7 +80,7 @@ install: $(TEST_PROGS) $(BINS)
 	rm -f *.o *.d *~
 
 distclean: clean
-	rm -f $(BINS)
+	rm -f $(TEST_GEN_PROGS) $(BINS)
 
 .PHONY: all install clean distclean run_tests
 .DEFAULT_GOAL=all
diff --git a/tools/net/ynl/tests/netdev.c b/tools/net/ynl/tests/netdev.c
index 22609d44c89a..6413e8166a7c 100644
--- a/tools/net/ynl/tests/netdev.c
+++ b/tools/net/ynl/tests/netdev.c
@@ -6,29 +6,29 @@
 
 #include <net/if.h>
 
+#include <kselftest_harness.h>
+
 #include "netdev-user.h"
+#include "rt-link-user.h"
 
-/* netdev genetlink family code sample
- * This sample shows off basics of the netdev family but also notification
- * handling, hence the somewhat odd UI. We subscribe to notifications first
- * then wait for ifc selection, so the socket may already accumulate
- * notifications as we wait. This allows us to test that YNL can handle
- * requests and notifications getting interleaved.
- */
-
-static void netdev_print_device(struct netdev_dev_get_rsp *d, unsigned int op)
+static void netdev_print_device(struct __test_metadata *_metadata,
+				struct netdev_dev_get_rsp *d, unsigned int op)
 {
 	char ifname[IF_NAMESIZE];
 	const char *name;
 
+	EXPECT_TRUE((bool)d->_present.ifindex);
 	if (!d->_present.ifindex)
 		return;
 
 	name = if_indextoname(d->ifindex, ifname);
+	EXPECT_TRUE((bool)name);
 	if (name)
-		printf("%8s", name);
-	printf("[%d]\t", d->ifindex);
+		ksft_print_msg("%8s[%d]\t", name, d->ifindex);
+	else
+		ksft_print_msg("[%d]\t", d->ifindex);
 
+	EXPECT_TRUE((bool)d->_present.xdp_features);
 	if (!d->_present.xdp_features)
 		return;
 
@@ -38,10 +38,12 @@ static void netdev_print_device(struct netdev_dev_get_rsp *d, unsigned int op)
 			printf(" %s", netdev_xdp_act_str(1 << i));
 	}
 
-	printf(" xdp-rx-metadata-features (%llx):", d->xdp_rx_metadata_features);
+	printf(" xdp-rx-metadata-features (%llx):",
+	       d->xdp_rx_metadata_features);
 	for (int i = 0; d->xdp_rx_metadata_features >= 1U << i; i++) {
 		if (d->xdp_rx_metadata_features & (1U << i))
-			printf(" %s", netdev_xdp_rx_metadata_str(1 << i));
+			printf(" %s",
+			       netdev_xdp_rx_metadata_str(1 << i));
 	}
 
 	printf(" xsk-features (%llx):", d->xsk_features);
@@ -58,71 +60,172 @@ static void netdev_print_device(struct netdev_dev_get_rsp *d, unsigned int op)
 	printf("\n");
 }
 
-int main(int argc, char **argv)
+static int veth_create(struct ynl_sock *ys_link)
+{
+	struct rt_link_getlink_ntf *ntf_gl;
+	struct rt_link_newlink_req *req;
+	struct ynl_ntf_base_type *ntf;
+	int ret;
+
+	req = rt_link_newlink_req_alloc();
+	if (!req)
+		return -1;
+
+	rt_link_newlink_req_set_nlflags(req, NLM_F_CREATE | NLM_F_ECHO);
+	rt_link_newlink_req_set_linkinfo_kind(req, "veth");
+
+	ret = rt_link_newlink(ys_link, req);
+	rt_link_newlink_req_free(req);
+	if (ret)
+		return -1;
+
+	if (!ynl_has_ntf(ys_link))
+		return 0;
+
+	ntf = ynl_ntf_dequeue(ys_link);
+	if (!ntf || ntf->cmd != RTM_NEWLINK) {
+		ynl_ntf_free(ntf);
+		return 0;
+	}
+	ntf_gl = (void *)ntf;
+	ret = ntf_gl->obj._hdr.ifi_index;
+	ynl_ntf_free(ntf);
+
+	return ret;
+}
+
+static void veth_delete(struct __test_metadata *_metadata,
+			struct ynl_sock *ys_link, int ifindex)
+{
+	struct rt_link_dellink_req *req;
+
+	req = rt_link_dellink_req_alloc();
+	if (!req)
+		return;
+
+	req->_hdr.ifi_index = ifindex;
+	EXPECT_EQ(0, rt_link_dellink(ys_link, req));
+	rt_link_dellink_req_free(req);
+}
+
+FIXTURE(netdev)
+{
+	struct ynl_sock *ys;
+	struct ynl_sock *ys_link;
+};
+
+FIXTURE_SETUP(netdev)
+{
+	struct ynl_error yerr;
+
+	self->ys = ynl_sock_create(&ynl_netdev_family, &yerr);
+	ASSERT_NE(NULL, self->ys) {
+		TH_LOG("Failed to create YNL netdev socket: %s", yerr.msg);
+	}
+}
+
+FIXTURE_TEARDOWN(netdev)
+{
+	if (self->ys_link)
+		ynl_sock_destroy(self->ys_link);
+	ynl_sock_destroy(self->ys);
+}
+
+TEST_F(netdev, dump)
 {
 	struct netdev_dev_get_list *devs;
-	struct ynl_ntf_base_type *ntf;
-	struct ynl_error yerr;
-	struct ynl_sock *ys;
+
+	devs = netdev_dev_get_dump(self->ys);
+	ASSERT_NE(NULL, devs) {
+		TH_LOG("dump failed: %s", self->ys->err.msg);
+	}
+
+	if (ynl_dump_empty(devs)) {
+		netdev_dev_get_list_free(devs);
+		SKIP(return, "no entries in dump");
+	}
+
+	ynl_dump_foreach(devs, d)
+		netdev_print_device(_metadata, d, 0);
+
+	netdev_dev_get_list_free(devs);
+}
+
+TEST_F(netdev, get)
+{
+	struct netdev_dev_get_list *devs;
+	struct netdev_dev_get_req *req;
+	struct netdev_dev_get_rsp *dev;
 	int ifindex = 0;
 
-	if (argc > 1)
-		ifindex = strtol(argv[1], NULL, 0);
-
-	ys = ynl_sock_create(&ynl_netdev_family, &yerr);
-	if (!ys) {
-		fprintf(stderr, "YNL: %s\n", yerr.msg);
-		return 1;
+	devs = netdev_dev_get_dump(self->ys);
+	ASSERT_NE(NULL, devs) {
+		TH_LOG("dump failed: %s", self->ys->err.msg);
 	}
 
-	if (ynl_subscribe(ys, "mgmt"))
-		goto err_close;
+	ynl_dump_foreach(devs, d) {
+		if (d->_present.ifindex) {
+			ifindex = d->ifindex;
+			break;
+		}
+	}
+	netdev_dev_get_list_free(devs);
 
-	printf("Select ifc ($ifindex; or 0 = dump; or -2 ntf check): ");
-	if (scanf("%d", &ifindex) != 1) {
-		fprintf(stderr, "Error: unable to parse input\n");
-		goto err_destroy;
+	if (!ifindex)
+		SKIP(return, "no device to query");
+
+	req = netdev_dev_get_req_alloc();
+	netdev_dev_get_req_set_ifindex(req, ifindex);
+
+	dev = netdev_dev_get(self->ys, req);
+	netdev_dev_get_req_free(req);
+	ASSERT_NE(NULL, dev) {
+		TH_LOG("dev_get failed: %s", self->ys->err.msg);
 	}
 
-	if (ifindex > 0) {
-		struct netdev_dev_get_req *req;
-		struct netdev_dev_get_rsp *d;
+	netdev_print_device(_metadata, dev, 0);
+	netdev_dev_get_rsp_free(dev);
+}
 
-		req = netdev_dev_get_req_alloc();
-		netdev_dev_get_req_set_ifindex(req, ifindex);
+TEST_F(netdev, ntf_check)
+{
+	struct ynl_ntf_base_type *ntf;
+	int veth_ifindex;
+	bool received;
+	int ret;
 
-		d = netdev_dev_get(ys, req);
-		netdev_dev_get_req_free(req);
-		if (!d)
-			goto err_close;
-
-		netdev_print_device(d, 0);
-		netdev_dev_get_rsp_free(d);
-	} else if (!ifindex) {
-		devs = netdev_dev_get_dump(ys);
-		if (!devs)
-			goto err_close;
-
-		if (ynl_dump_empty(devs))
-			fprintf(stderr, "Error: no devices reported\n");
-		ynl_dump_foreach(devs, d)
-			netdev_print_device(d, 0);
-		netdev_dev_get_list_free(devs);
-	} else if (ifindex == -2) {
-		ynl_ntf_check(ys);
+	ret = ynl_subscribe(self->ys, "mgmt");
+	ASSERT_EQ(0, ret) {
+		TH_LOG("subscribe failed: %s", self->ys->err.msg);
 	}
-	while ((ntf = ynl_ntf_dequeue(ys))) {
-		netdev_print_device((struct netdev_dev_get_rsp *)&ntf->data,
+
+	self->ys_link = ynl_sock_create(&ynl_rt_link_family, NULL);
+	ASSERT_NE(NULL, self->ys_link)
+		TH_LOG("failed to create rt-link socket");
+
+	veth_ifindex = veth_create(self->ys_link);
+	ASSERT_GT(veth_ifindex, 0)
+		TH_LOG("failed to create veth");
+
+	ynl_ntf_check(self->ys);
+
+	ntf = ynl_ntf_dequeue(self->ys);
+	received = ntf;
+	if (ntf) {
+		netdev_print_device(_metadata,
+				    (struct netdev_dev_get_rsp *)&ntf->data,
 				    ntf->cmd);
 		ynl_ntf_free(ntf);
 	}
 
-	ynl_sock_destroy(ys);
-	return 0;
+	/* Drain any remaining notifications */
+	while ((ntf = ynl_ntf_dequeue(self->ys)))
+		ynl_ntf_free(ntf);
 
-err_close:
-	fprintf(stderr, "YNL: %s\n", ys->err.msg);
-err_destroy:
-	ynl_sock_destroy(ys);
-	return 2;
+	veth_delete(_metadata, self->ys_link, veth_ifindex);
+
+	ASSERT_TRUE(received)
+		TH_LOG("no notification received");
 }
+
+TEST_HARNESS_MAIN
-- 
2.53.0