Linux Documentation
 help / color / mirror / Atom feed
From: Konstantin Sinyuk <ksinyuk@kernel.org>
To: dri-devel@lists.freedesktop.org
Cc: Maarten Lankhorst <maarten.lankhorst@linux.intel.com>,
	Francois Dugast <francois.dugast@intel.com>,
	David Airlie <airlied@gmail.com>, Simona Vetter <simona@ffwll.ch>,
	Maxime Ripard <mripard@kernel.org>,
	Thomas Zimmermann <tzimmermann@suse.de>,
	Jonathan Corbet <corbet@lwn.net>,
	Shuah Khan <skhan@linuxfoundation.org>,
	Donald Hunter <donald.hunter@gmail.com>,
	Jakub Kicinski <kuba@kernel.org>,
	"David S. Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@google.com>,
	Paolo Abeni <pabeni@redhat.com>, Simon Horman <horms@kernel.org>,
	Ilia Levi <ilia.levi@intel.com>,
	Rodrigo Vivi <rodrigo.vivi@intel.com>,
	linux-doc@vger.kernel.org, linux-kselftest@vger.kernel.org,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [RFC PATCH 05/12] drm/fabric: add object-model KUnit tests
Date: Mon, 24 Aug 2026 11:09:33 +0300	[thread overview]
Message-ID: <35955cefe73d0dd9d7553542ece17dfb65c04576.1787552412.git.ksinyuk@kernel.org> (raw)
In-Reply-To: <cover.1787552412.git.ksinyuk@kernel.org>

The object model has invariants a refactor can break: identity scope,
borrowed port lifetime, topology-generation updates and the context in
which provider callbacks run.

Build the tests into drm_fabric.o under CONFIG_DRM_FABRIC_KUNIT_TEST so
no test-only symbols need to be exported.

Coverage includes registration and identity uniqueness, object lifetime,
half-edge peer state, topology-generation changes and provider ops
wiring. Linear and mesh topologies are parameterized. Unregister rejects
a non-member pointer, including one carrying a registered object's
identifier.

Provide .kunitconfig for UML and .kunitconfig.debug for x86-64 with
KASAN, KMEMLEAK, UBSAN, PROVE_LOCKING and DEBUG_ATOMIC_SLEEP.

  $ ./tools/testing/kunit/kunit.py run \
        --kunitconfig=drivers/gpu/drm/fabric/.kunitconfig
  [...]
  Testing complete. Ran 31 tests: passed: 31

Signed-off-by: Konstantin Sinyuk <ksinyuk@kernel.org>
Assisted-by: GitHub-Copilot:claude-opus-4.8
---
 Documentation/gpu/drm-fabric.rst          |    6 +
 drivers/gpu/drm/fabric/.kunitconfig       |    5 +
 drivers/gpu/drm/fabric/.kunitconfig.debug |   16 +
 drivers/gpu/drm/fabric/Kconfig            |   16 +
 drivers/gpu/drm/fabric/Makefile           |    2 +
 drivers/gpu/drm/fabric/drm_fabric.c       |    4 +
 drivers/gpu/drm/fabric/drm_fabric_test.c  | 1312 +++++++++++++++++++++
 7 files changed, 1361 insertions(+)
 create mode 100644 drivers/gpu/drm/fabric/.kunitconfig
 create mode 100644 drivers/gpu/drm/fabric/.kunitconfig.debug
 create mode 100644 drivers/gpu/drm/fabric/drm_fabric_test.c

diff --git a/Documentation/gpu/drm-fabric.rst b/Documentation/gpu/drm-fabric.rst
index 8bd3633d41be..cd98a97e1662 100644
--- a/Documentation/gpu/drm-fabric.rst
+++ b/Documentation/gpu/drm-fabric.rst
@@ -351,3 +351,9 @@ and runtime endpoint add/remove. These files are unstable test controls and are
 not part of the uAPI; the stable, reviewed interface is the YAML-described
 Generic Netlink family. Tests mutate simulator state through debugfs and observe
 the result over Generic Netlink.
+
+Testing
+=======
+
+The object model is covered by KUnit when ``CONFIG_DRM_FABRIC_KUNIT_TEST`` is
+enabled. The test source is folded into the core translation unit.
diff --git a/drivers/gpu/drm/fabric/.kunitconfig b/drivers/gpu/drm/fabric/.kunitconfig
new file mode 100644
index 000000000000..d6087f45f2ac
--- /dev/null
+++ b/drivers/gpu/drm/fabric/.kunitconfig
@@ -0,0 +1,5 @@
+CONFIG_KUNIT=y
+CONFIG_NET=y
+CONFIG_DRM=y
+CONFIG_DRM_FABRIC=y
+CONFIG_DRM_FABRIC_KUNIT_TEST=y
diff --git a/drivers/gpu/drm/fabric/.kunitconfig.debug b/drivers/gpu/drm/fabric/.kunitconfig.debug
new file mode 100644
index 000000000000..8add027fc766
--- /dev/null
+++ b/drivers/gpu/drm/fabric/.kunitconfig.debug
@@ -0,0 +1,16 @@
+CONFIG_KUNIT=y
+CONFIG_NET=y
+CONFIG_DRM=y
+CONFIG_DRM_FABRIC=y
+CONFIG_DRM_FABRIC_KUNIT_TEST=y
+CONFIG_DEBUG_KERNEL=y
+CONFIG_KASAN=y
+CONFIG_KASAN_GENERIC=y
+CONFIG_DEBUG_KMEMLEAK=y
+CONFIG_PROVE_LOCKING=y
+CONFIG_DEBUG_ATOMIC_SLEEP=y
+CONFIG_UBSAN=y
+CONFIG_UBSAN_BOUNDS=y
+CONFIG_UBSAN_SHIFT=y
+CONFIG_UBSAN_ENUM=y
+CONFIG_UBSAN_BOOL=y
diff --git a/drivers/gpu/drm/fabric/Kconfig b/drivers/gpu/drm/fabric/Kconfig
index 87115356baca..2a70cac85b1d 100644
--- a/drivers/gpu/drm/fabric/Kconfig
+++ b/drivers/gpu/drm/fabric/Kconfig
@@ -22,3 +22,19 @@ config DRM_FABRIC_SIM
 	  error injection, and port-state transitions.  These hooks are
 	  NOT part of the drm_fabric uAPI and are used only by selftests.
 	  Does not model UALink protocol traffic or memory semantics.
+
+config DRM_FABRIC_KUNIT_TEST
+	bool "DRM fabric object-model KUnit tests" if !KUNIT_ALL_TESTS
+	depends on DRM_FABRIC && KUNIT
+	depends on KUNIT=y || DRM_FABRIC=m
+	default KUNIT_ALL_TESTS
+	help
+	  Enable KUnit coverage for the drm_fabric object model.
+
+	  The tests are built into drm_fabric itself, so they need no exported
+	  symbols or test-only accessors in the production source.
+
+	  For more information on KUnit and unit tests in general, please refer
+	  to the KUnit documentation in Documentation/dev-tools/kunit/.
+
+	  If unsure, say N.
diff --git a/drivers/gpu/drm/fabric/Makefile b/drivers/gpu/drm/fabric/Makefile
index bc0a6c742164..05f1c96bb8de 100644
--- a/drivers/gpu/drm/fabric/Makefile
+++ b/drivers/gpu/drm/fabric/Makefile
@@ -5,3 +5,5 @@ drm-fabric-y := drm_fabric.o drm_fabric_netlink.o drm_fabric_nl.o
 
 obj-$(CONFIG_DRM_FABRIC_SIM) += drm-fabric-sim.o
 drm-fabric-sim-y := drm_fabric_sim.o
+
+# The tests are included in drm_fabric.o, not built as a separate object.
diff --git a/drivers/gpu/drm/fabric/drm_fabric.c b/drivers/gpu/drm/fabric/drm_fabric.c
index ce331656af70..bbc3224c3314 100644
--- a/drivers/gpu/drm/fabric/drm_fabric.c
+++ b/drivers/gpu/drm/fabric/drm_fabric.c
@@ -690,3 +690,7 @@ module_exit(drm_fabric_exit);
 MODULE_AUTHOR("Intel Corporation");
 MODULE_DESCRIPTION("DRM fabric infrastructure");
 MODULE_LICENSE("Dual MIT/GPL");
+
+#if IS_ENABLED(CONFIG_DRM_FABRIC_KUNIT_TEST)
+#include "drm_fabric_test.c"
+#endif
diff --git a/drivers/gpu/drm/fabric/drm_fabric_test.c b/drivers/gpu/drm/fabric/drm_fabric_test.c
new file mode 100644
index 000000000000..3457675645f6
--- /dev/null
+++ b/drivers/gpu/drm/fabric/drm_fabric_test.c
@@ -0,0 +1,1312 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+/*
+ * KUnit tests for DRM Fabric object lifetime, identity, topology and
+ * provider callback registration.
+ */
+
+#include <kunit/test.h>
+#include <kunit/device.h>
+
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/mutex.h>
+#include <linux/string.h>
+
+#include <drm/drm_fabric.h>
+#include <uapi/drm/drm_fabric.h>
+
+#include "drm_fabric_internal.h"
+
+static struct drm_fabric_port *fabrictest_port(struct drm_fabric_endpoint *ep,
+					       u32 index)
+{
+	return drm_fabric_endpoint_port(ep, index);
+}
+
+static struct device *fabrictest_alloc_dev(struct kunit *test)
+{
+	struct device *dev = kunit_device_register(test, "drm_fabric_test");
+
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
+	return dev;
+}
+
+static void fabrictest_unregister_fabric(void *fab)
+{
+	drm_fabric_unregister(fab);
+}
+
+static void fabrictest_unregister_endpoint(void *ep)
+{
+	drm_fabric_endpoint_unregister(ep);
+}
+
+/* Production reads the generation under the lock; do the same here. */
+static u32 fabrictest_seq_read(void)
+{
+	guard(mutex)(&drm_fabric_lock);
+
+	return drm_fabric_base_seq;
+}
+
+static void fabrictest_seq_write(u32 val)
+{
+	guard(mutex)(&drm_fabric_lock);
+
+	drm_fabric_base_seq = val;
+}
+
+static void fabrictest_restore_seq(void *saved)
+{
+	fabrictest_seq_write(*(u32 *)saved);
+}
+
+/* Restore on exit so a seeded value cannot leak into a later test. */
+static void fabrictest_seed_seq(struct kunit *test, u32 val)
+{
+	u32 *saved = kunit_kzalloc(test, sizeof(*saved), GFP_KERNEL);
+
+	KUNIT_ASSERT_NOT_NULL(test, saved);
+	*saved = fabrictest_seq_read();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_restore_seq, saved));
+
+	fabrictest_seq_write(val);
+}
+
+static void drm_fabric_test_fabric_register(struct kunit *test)
+{
+	struct drm_fabric_desc desc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-fabric",
+		.instance_id = 0xDEAD,
+	};
+	struct drm_fabric *fab;
+
+	fab = drm_fabric_register(&desc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	KUNIT_EXPECT_EQ(test, fab->type, DRM_FABRIC_TYPE_SYNTHETIC);
+	KUNIT_EXPECT_EQ(test, fab->instance_id, 0xDEADULL);
+}
+
+/*
+ * The non-empty case warns because it is a provider teardown bug, so it is not
+ * exercised here.
+ */
+static void drm_fabric_test_unregister_reports_removal(struct kunit *test)
+{
+	struct drm_fabric_desc desc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "unreg-ret",
+	};
+	struct drm_fabric *fab;
+
+	fab = drm_fabric_register(&desc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_EXPECT_EQ(test, drm_fabric_unregister(fab), 0);
+}
+
+static void drm_fabric_test_endpoint_requires_fabric(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 1,
+		.name = "no-fabric",
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_endpoint *ep;
+
+	ep = drm_fabric_endpoint_register(NULL, &edesc);
+	KUNIT_EXPECT_TRUE(test, IS_ERR(ep));
+	if (IS_ERR(ep))
+		KUNIT_EXPECT_EQ(test, PTR_ERR(ep), -EINVAL);
+}
+
+/* Registration must not walk a NULL port array. */
+static void drm_fabric_test_endpoint_requires_port_array(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "no-port-array",
+	};
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 1,
+		.name = "portless",
+		.parent = fabrictest_dev,
+		.ports = NULL,
+		.num_ports = 1,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_EXPECT_TRUE(test, IS_ERR(ep));
+	if (IS_ERR(ep))
+		KUNIT_EXPECT_EQ(test, PTR_ERR(ep), -EINVAL);
+}
+
+/* Reject a non-member pointer without dereferencing it. */
+static void drm_fabric_test_unregister_rejects_non_member(struct kunit *test)
+{
+	struct drm_fabric *candidate;
+	u32 before;
+
+	candidate = kunit_kzalloc(test, sizeof(*candidate), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, candidate);
+
+	before = fabrictest_seq_read();
+	KUNIT_EXPECT_EQ(test, drm_fabric_unregister(candidate), -ENODEV);
+	/* A rejected teardown emits nothing, so the generation cannot move. */
+	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), before);
+}
+
+/*
+ * The check must compare the supplied pointer, not resolve the id it carries:
+ * an id-keyed lookup would erase whichever live fabric owns that id.
+ */
+static void drm_fabric_test_unregister_rejects_same_id(struct kunit *test)
+{
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "same-id-owner",
+	};
+	struct drm_fabric *fab, *twin;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	twin = kunit_kzalloc(test, sizeof(*twin), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, twin);
+	twin->id = fab->id;
+
+	KUNIT_EXPECT_EQ(test, drm_fabric_unregister(twin), -ENODEV);
+
+	/* Returning 0 proves the impostor did not erase the id's owner. */
+	kunit_remove_action(test, fabrictest_unregister_fabric, fab);
+	KUNIT_EXPECT_EQ(test, drm_fabric_unregister(fab), 0);
+}
+
+/*
+ * The endpoint registry is checked directly rather than through
+ * drm_fabric_endpoint_unregister(), whose only report channel for an
+ * unregistered pointer is a one-shot warning.
+ */
+static void drm_fabric_test_ep_membership_rejects_non_member(struct kunit *test)
+{
+	struct drm_fabric_endpoint *ep;
+
+	ep = kunit_kzalloc(test, sizeof(*ep), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ep);
+
+	scoped_guard(mutex, &drm_fabric_lock)
+		KUNIT_EXPECT_FALSE(test, drm_fabric_ep_is_registered(ep));
+}
+
+/* Endpoint counterpart of the same-id case: membership is pointer identity. */
+static void drm_fabric_test_ep_membership_rejects_same_id(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "ep-same-id-fab",
+	};
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 7,
+		.name = "ep-same-id-owner",
+		.parent = fabrictest_dev,
+	};
+	struct drm_fabric_endpoint *ep, *twin;
+	struct drm_fabric *fab;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	twin = kunit_kzalloc(test, sizeof(*twin), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, twin);
+	twin->id = ep->id;
+
+	scoped_guard(mutex, &drm_fabric_lock) {
+		KUNIT_EXPECT_FALSE(test, drm_fabric_ep_is_registered(twin));
+		KUNIT_EXPECT_TRUE(test, drm_fabric_ep_is_registered(ep));
+	}
+}
+
+/*
+ * Collapses the register-vs-unregister race: unregister first, then attach to
+ * the stale pointer.
+ */
+static void drm_fabric_test_endpoint_register_stale_fabric(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "stale-parent",
+	};
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 1,
+		.name = "orphaned-by-unreg",
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, drm_fabric_unregister(fab), 0);
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_EXPECT_TRUE(test, IS_ERR(ep));
+	if (IS_ERR(ep))
+		KUNIT_EXPECT_EQ(test, PTR_ERR(ep), -ENODEV);
+}
+
+static void drm_fabric_test_instance_id_unique(struct kunit *test)
+{
+	struct drm_fabric_desc a = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "iid-a",
+		.instance_id = 0x1357,
+	};
+	struct drm_fabric_desc dup = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "iid-dup",
+		.instance_id = 0x1357,
+	};
+	struct drm_fabric_desc zero_a = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "iid-z0", .instance_id = 0,
+	};
+	struct drm_fabric_desc zero_b = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "iid-z1", .instance_id = 0,
+	};
+	struct drm_fabric *fab, *fab2, *dupf;
+
+	fab = drm_fabric_register(&a);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	dupf = drm_fabric_register(&dup);
+	KUNIT_EXPECT_TRUE(test, IS_ERR(dupf));
+	if (IS_ERR(dupf))
+		KUNIT_EXPECT_EQ(test, PTR_ERR(dupf), -EEXIST);
+
+	fab = drm_fabric_register(&zero_a);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+	/* instance_id 0 is not special-cased. */
+	fab2 = drm_fabric_register(&zero_b);
+	KUNIT_EXPECT_TRUE(test, IS_ERR(fab2));
+	if (IS_ERR(fab2))
+		KUNIT_EXPECT_EQ(test, PTR_ERR(fab2), -EEXIST);
+}
+
+static void drm_fabric_test_endpoint_register(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-ep-fab",
+	};
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x42,
+		.name = "ep0",
+		.parent = fabrictest_dev,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	KUNIT_EXPECT_PTR_EQ(test, ep->fabric, fab);
+	KUNIT_EXPECT_EQ(test, ep->fabric_ep_id, 0x42ULL);
+}
+
+static void drm_fabric_test_port_register_peer(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-port-fab",
+	};
+	struct drm_fabric_port_desc pdesc = {
+		.index = 0,
+		.max_lane_count = 4,
+		.max_lane_signaling_rate_mbps = 200000,
+	};
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x10,
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_peer peer = {
+		.peer_id = 0x20,
+		.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+		.port_index = 1,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+	struct drm_fabric_port *port;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	port = fabrictest_port(ep, 0);
+	KUNIT_ASSERT_NOT_NULL(test, port);
+
+	KUNIT_EXPECT_EQ(test, port->index, 0);
+	KUNIT_EXPECT_EQ(test, port->max_lane_count, 4);
+	KUNIT_EXPECT_FALSE(test, port->has_peer);
+	KUNIT_EXPECT_EQ(test, ep->num_ports, 1);
+
+	KUNIT_EXPECT_EQ(test, drm_fabric_port_set_peer(port, &peer), 0);
+	KUNIT_EXPECT_TRUE(test, port->has_peer);
+	KUNIT_EXPECT_EQ(test, port->peer.peer_id, 0x20ULL);
+	KUNIT_EXPECT_EQ(test, port->peer.port_index, 1);
+}
+
+/* Topology changes must invalidate an in-progress dump. */
+static void drm_fabric_test_base_seq_advances(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-base-seq",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 2 };
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x40,
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_peer peer = {
+		.peer_id = 0x41,
+		.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+		.port_index = 0,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+	struct drm_fabric_port *port;
+	u32 seq;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	port = fabrictest_port(ep, 0);
+	KUNIT_ASSERT_NOT_NULL(test, port);
+
+	seq = fabrictest_seq_read();
+	KUNIT_EXPECT_EQ(test, drm_fabric_port_set_peer(port, &peer), 0);
+	KUNIT_EXPECT_NE(test, fabrictest_seq_read(), seq);
+
+	seq = fabrictest_seq_read();
+	KUNIT_EXPECT_EQ(test, drm_fabric_port_unset_peer(port), 0);
+	KUNIT_EXPECT_NE(test, fabrictest_seq_read(), seq);
+
+	seq = fabrictest_seq_read();
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+	KUNIT_EXPECT_NE(test, fabrictest_seq_read(), seq);
+
+	/* A no-op transition must not advance the generation. */
+	seq = fabrictest_seq_read();
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), seq);
+}
+
+/* The generation is a nonzero u32: wrapping must skip 0, not just increment. */
+static void drm_fabric_test_generation_wrap_nonzero(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-gen-wrap",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 2 };
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x4A,
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+	struct drm_fabric_port *port;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	port = fabrictest_port(ep, 0);
+	KUNIT_ASSERT_NOT_NULL(test, port);
+
+	/* Make the next state change advance the generation once. */
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_INACTIVE);
+
+	fabrictest_seed_seq(test, U32_MAX);
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+	KUNIT_EXPECT_NE(test, fabrictest_seq_read(), 0);
+	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), 1);
+}
+
+static void drm_fabric_test_port_state_transition(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-state",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 2 };
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x30,
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+	struct drm_fabric_port *port;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	port = fabrictest_port(ep, 0);
+	KUNIT_ASSERT_NOT_NULL(test, port);
+
+	KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_UNKNOWN);
+
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+	KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_ACTIVE);
+
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_DEGRADED);
+	KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_DEGRADED);
+
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_INACTIVE);
+	KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_INACTIVE);
+}
+
+/* An invalid provider state must not be committed. */
+static void drm_fabric_test_port_oper_state_rejects_invalid(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-state-invalid",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 2 };
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x32,
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	enum drm_fabric_port_state bad =
+		(enum drm_fabric_port_state)(DRM_FABRIC_PORT_STATE_DEGRADED + 1);
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+	struct drm_fabric_port *port;
+	u32 seq;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	port = fabrictest_port(ep, 0);
+	KUNIT_ASSERT_NOT_NULL(test, port);
+
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_ACTIVE);
+	KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_ACTIVE);
+
+	seq = fabrictest_seq_read();
+	drm_fabric_port_set_oper(port, bad);
+	KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_ACTIVE);
+	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), seq);
+
+	drm_fabric_port_set_oper(port, DRM_FABRIC_PORT_STATE_INACTIVE);
+	KUNIT_EXPECT_EQ(test, port->oper_state, DRM_FABRIC_PORT_STATE_INACTIVE);
+}
+
+static void drm_fabric_test_register_rejects_invalid_type(struct kunit *test)
+{
+	struct drm_fabric_desc above = {
+		.type = (enum drm_fabric_type)(DRM_FABRIC_TYPE_SYNTHETIC + 1),
+		.instance_id = 0x5a5a,
+		.name = "test-type-above",
+	};
+	struct drm_fabric_desc zero = {
+		.type = (enum drm_fabric_type)0,
+		.instance_id = 0x5a5a,
+		.name = "test-type-zero",
+	};
+	struct drm_fabric_desc good = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.instance_id = 0x5a5a,
+		.name = "test-type-good",
+	};
+	struct drm_fabric *fab, *bad;
+	u32 seq = fabrictest_seq_read();
+
+	bad = drm_fabric_register(&above);
+	KUNIT_EXPECT_TRUE(test, IS_ERR(bad));
+	if (IS_ERR(bad))
+		KUNIT_EXPECT_EQ(test, PTR_ERR(bad), -EINVAL);
+	else
+		drm_fabric_unregister(bad);
+
+	bad = drm_fabric_register(&zero);
+	KUNIT_EXPECT_TRUE(test, IS_ERR(bad));
+	if (IS_ERR(bad))
+		KUNIT_EXPECT_EQ(test, PTR_ERR(bad), -EINVAL);
+	else
+		drm_fabric_unregister(bad);
+
+	/* Refused before publication, so the generation cannot have advanced. */
+	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), seq);
+
+	/*
+	 * Same instance id as both refusals: drm_fabric_has_instance() would
+	 * answer -EEXIST here if either had left a registry entry behind.
+	 */
+	fab = drm_fabric_register(&good);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+}
+
+/* Verify reciprocity and far-side port indices for every K_4 edge. */
+static void drm_fabric_test_mesh_kn_topology(struct kunit *test)
+{
+#define KN_EPS 4
+#define KN_PORTS_PER_EP (KN_EPS - 1)
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "mesh-kn",
+		.instance_id = 0x1002,
+	};
+	struct drm_fabric_port_desc pdescs[KN_PORTS_PER_EP];
+	struct drm_fabric_endpoint_desc edesc;
+	struct drm_fabric_peer peer;
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *eps[KN_EPS];
+	struct drm_fabric_port *ports[KN_EPS][KN_PORTS_PER_EP];
+	int i, j, k, port_idx, peer_port;
+
+	memset(pdescs, 0, sizeof(pdescs));
+	for (j = 0; j < KN_PORTS_PER_EP; j++) {
+		pdescs[j].index = j;
+		pdescs[j].max_lane_count = 4;
+		pdescs[j].max_lane_signaling_rate_mbps = 200000;
+	}
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	for (i = 0; i < KN_EPS; i++) {
+		memset(&edesc, 0, sizeof(edesc));
+		edesc.fabric_ep_id = 0x100 + i;
+		edesc.parent = fabrictest_dev;
+		edesc.ports = pdescs;
+		edesc.num_ports = KN_PORTS_PER_EP;
+
+		eps[i] = drm_fabric_endpoint_register(fab, &edesc);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(eps[i]));
+		KUNIT_ASSERT_EQ(test, 0,
+				kunit_add_action_or_reset(test,
+							  fabrictest_unregister_endpoint, eps[i]));
+
+		for (j = 0; j < KN_PORTS_PER_EP; j++) {
+			ports[i][j] = fabrictest_port(eps[i], j);
+			KUNIT_ASSERT_NOT_NULL(test, ports[i][j]);
+		}
+	}
+
+	/* Wire K_4: ep[i] port[j] -> ep[target] */
+	for (i = 0; i < KN_EPS; i++) {
+		port_idx = 0;
+		for (j = 0; j < KN_EPS; j++) {
+			if (i == j)
+				continue;
+
+			/* Compute peer's port index pointing back to us. */
+			peer_port = 0;
+			for (k = 0; k < KN_EPS; k++) {
+				if (k == j)
+					continue;
+				if (k == i)
+					break;
+				peer_port++;
+			}
+
+			peer.peer_id = eps[j]->fabric_ep_id;
+			peer.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL;
+			peer.port_index = peer_port;
+			drm_fabric_port_set_peer(ports[i][port_idx], &peer);
+			port_idx++;
+		}
+	}
+
+	for (i = 0; i < KN_EPS; i++) {
+		for (j = 0; j < KN_PORTS_PER_EP; j++) {
+			struct drm_fabric_port *p = ports[i][j];
+			struct drm_fabric_port *pp;
+			u64 peer_ep_id;
+			u32 peer_pidx;
+			int peer_ep_idx, pi;
+
+			KUNIT_EXPECT_TRUE_MSG(test, p->has_peer,
+					      "ep%d port%d has no peer", i, j);
+			if (!p->has_peer)
+				continue;
+
+			peer_ep_id = p->peer.peer_id;
+			peer_pidx = p->peer.port_index;
+
+			peer_ep_idx = -1;
+			for (pi = 0; pi < KN_EPS; pi++) {
+				if (eps[pi]->fabric_ep_id == peer_ep_id) {
+					peer_ep_idx = pi;
+					break;
+				}
+			}
+			KUNIT_EXPECT_GE_MSG(test, peer_ep_idx, 0,
+					    "ep%d port%d peer EP not found", i, j);
+			if (peer_ep_idx < 0)
+				continue;
+			KUNIT_EXPECT_LT(test, peer_pidx, (u32)KN_PORTS_PER_EP);
+			if (peer_pidx >= (u32)KN_PORTS_PER_EP)
+				continue;
+
+			pp = ports[peer_ep_idx][peer_pidx];
+			KUNIT_EXPECT_TRUE(test, pp->has_peer);
+			KUNIT_EXPECT_EQ(test, pp->peer.peer_id, eps[i]->fabric_ep_id);
+			KUNIT_EXPECT_EQ(test, pp->peer.port_index, (u32)j);
+		}
+	}
+#undef KN_EPS
+#undef KN_PORTS_PER_EP
+}
+
+static int fabrictest_stats_get(struct drm_fabric_port *port,
+				struct drm_fabric_port_stats *stats)
+{
+	stats->read_bytes = 4096;
+	stats->write_bytes = 2048;
+	stats->link_down_count = 2;
+	stats->retrain_count = 3;
+	return 0;
+}
+
+static const struct drm_fabric_ops fabrictest_stats_ops = {
+	.port_stats_get = fabrictest_stats_get,
+};
+
+/* This does not exercise netlink dispatch or error propagation. */
+static void drm_fabric_test_port_stats_ops_registration(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-stats",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0x5A,
+		.parent = fabrictest_dev,
+		.ops = &fabrictest_stats_ops,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_endpoint_desc edesc_noops = {
+		.fabric_ep_id = 0x5B,
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_port_stats stats = {};
+	struct drm_fabric_endpoint *ep, *ep_noops;
+	struct drm_fabric_port *port;
+	struct drm_fabric *fab;
+	u32 gen;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	port = fabrictest_port(ep, 0);
+	KUNIT_ASSERT_NOT_NULL(test, port);
+
+	KUNIT_ASSERT_NOT_NULL(test, ep->ops);
+	KUNIT_ASSERT_NOT_NULL(test, ep->ops->port_stats_get);
+
+	/* A stats read is not a topology change: seq must not move. */
+	gen = fabrictest_seq_read();
+	KUNIT_EXPECT_EQ(test, ep->ops->port_stats_get(port, &stats), 0);
+	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), gen);
+	KUNIT_EXPECT_EQ(test, stats.read_bytes, 4096ULL);
+	KUNIT_EXPECT_EQ(test, stats.write_bytes, 2048ULL);
+	KUNIT_EXPECT_EQ(test, stats.link_down_count, 2ULL);
+	KUNIT_EXPECT_EQ(test, stats.retrain_count, 3ULL);
+
+	ep_noops = drm_fabric_endpoint_register(fab, &edesc_noops);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_noops));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_noops));
+	KUNIT_EXPECT_TRUE(test, !ep_noops->ops || !ep_noops->ops->port_stats_get);
+}
+
+/*
+ * Unregistering an endpoint that has a peer link must clear only that
+ * endpoint's own port record; it must not touch the still-registered far
+ * side's peer record. (Contrast with drm_fabric_port_unset_peer(), which
+ * clears a peer explicitly and is covered separately.)
+ */
+static void drm_fabric_test_local_unplug_keeps_edge(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-unplug",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc eadesc = {
+		.fabric_ep_id = 0xA0,
+		.name = "unplug-a",
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_endpoint_desc ebdesc = {
+		.fabric_ep_id = 0xB0,
+		.name = "unplug-b",
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_peer to_b = {
+		.peer_id = 0xB0, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+	};
+	struct drm_fabric_peer to_a = {
+		.peer_id = 0xA0, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep_a, *ep_b;
+	struct drm_fabric_port *pa, *pb;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep_a = drm_fabric_endpoint_register(fab, &eadesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_a));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_a));
+
+	ep_b = drm_fabric_endpoint_register(fab, &ebdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_b));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_b));
+
+	pa = fabrictest_port(ep_a, 0);
+	pb = fabrictest_port(ep_b, 0);
+	KUNIT_ASSERT_NOT_NULL(test, pa);
+	KUNIT_ASSERT_NOT_NULL(test, pb);
+
+	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pa, &to_b), 0);
+	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pb, &to_a), 0);
+	KUNIT_EXPECT_TRUE(test, pa->has_peer);
+
+	/*
+	 * Remove B without retracting its peer first, modelling abrupt provider
+	 * teardown.
+	 */
+	kunit_release_action(test, fabrictest_unregister_endpoint, ep_b);
+
+	/* The surviving half-edge must be byte-unchanged: no field mutated. */
+	KUNIT_EXPECT_TRUE(test, pa->has_peer);
+	KUNIT_EXPECT_MEMEQ(test, &pa->peer, &to_b, sizeof(pa->peer));
+}
+
+/*
+ * A's peer record names a port index, not an object; registering and then
+ * unregistering an unrelated third endpoint must not perturb it.
+ */
+static void drm_fabric_test_remote_peer_retained(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-remote",
+	};
+	struct drm_fabric_port_desc pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc eadesc = {
+		.fabric_ep_id = 0xA0,
+		.name = "remote-a",
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	struct drm_fabric_endpoint_desc ecdesc = {
+		.fabric_ep_id = 0xC0,
+		.name = "remote-c",
+		.parent = fabrictest_dev,
+		.ports = &pdesc,
+		.num_ports = 1,
+	};
+	/* 0xBEEF has no local endpoint object. */
+	struct drm_fabric_peer remote = {
+		.peer_id = 0xBEEF, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep_a, *ep_c;
+	struct drm_fabric_port *pa;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep_a = drm_fabric_endpoint_register(fab, &eadesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_a));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_a));
+
+	pa = fabrictest_port(ep_a, 0);
+	KUNIT_ASSERT_NOT_NULL(test, pa);
+
+	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(pa, &remote), 0);
+	KUNIT_EXPECT_TRUE(test, pa->has_peer);
+
+	ep_c = drm_fabric_endpoint_register(fab, &ecdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep_c));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep_c));
+
+	kunit_release_action(test, fabrictest_unregister_endpoint, ep_c);
+
+	KUNIT_EXPECT_TRUE(test, pa->has_peer);
+	KUNIT_EXPECT_MEMEQ(test, &pa->peer, &remote, sizeof(pa->peer));
+}
+
+/*
+ * Removing an endpoint with multiple peered ports must bump the topology
+ * generation exactly once, not once per port torn down.
+ */
+static void drm_fabric_test_subtree_delete_single_bump(struct kunit *test)
+{
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-subtree",
+	};
+	struct drm_fabric_port_desc pdescs[3] = {
+		{ .index = 0, .max_lane_count = 4 },
+		{ .index = 1, .max_lane_count = 4 },
+		{ .index = 2, .max_lane_count = 4 },
+	};
+	struct drm_fabric_endpoint_desc edesc = {
+		.fabric_ep_id = 0xD0,
+		.name = "subtree-ep",
+		.parent = fabrictest_dev,
+		.ports = pdescs,
+		.num_ports = 3,
+	};
+	struct drm_fabric_peer peer = {
+		.peer_id = 0xD1, .peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+	};
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *ep;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	ep = drm_fabric_endpoint_register(fab, &edesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ep));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_endpoint, ep));
+
+	/* Two of the three ports carry a half-edge. */
+	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(fabrictest_port(ep, 0), &peer), 0);
+	KUNIT_ASSERT_EQ(test, drm_fabric_port_set_peer(fabrictest_port(ep, 1), &peer), 0);
+
+	fabrictest_seed_seq(test, 100);
+	kunit_release_action(test, fabrictest_unregister_endpoint, ep);
+	KUNIT_EXPECT_EQ(test, fabrictest_seq_read(), 101);
+}
+
+static void drm_fabric_test_switch_topology(struct kunit *test)
+{
+#define SW_LEAVES 3
+	/*
+	 * Each leaf carries one half-edge to an opaque switch that is not a
+	 * registered endpoint, so this asserts half-edge serialization, not any
+	 * leaf -> switch -> leaf traversal.
+	 */
+	const u64 sw_id = 0x5000;
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC,
+		.name = "test-switch",
+	};
+	struct drm_fabric_port_desc leaf_pdesc = { .index = 0, .max_lane_count = 4 };
+	struct drm_fabric_endpoint_desc edesc;
+	struct drm_fabric_peer peer;
+	struct drm_fabric *fab;
+	struct drm_fabric_endpoint *leaves[SW_LEAVES];
+	struct drm_fabric_port *leaf_ports[SW_LEAVES];
+	int i;
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	for (i = 0; i < SW_LEAVES; i++) {
+		memset(&edesc, 0, sizeof(edesc));
+		edesc.fabric_ep_id = 0x600 + i;
+		edesc.parent = fabrictest_dev;
+		edesc.ports = &leaf_pdesc;
+		edesc.num_ports = 1;
+		leaves[i] = drm_fabric_endpoint_register(fab, &edesc);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(leaves[i]));
+		KUNIT_ASSERT_EQ(test, 0,
+				kunit_add_action_or_reset(test,
+							  fabrictest_unregister_endpoint,
+							  leaves[i]));
+
+		leaf_ports[i] = fabrictest_port(leaves[i], 0);
+		KUNIT_ASSERT_NOT_NULL(test, leaf_ports[i]);
+
+		peer.peer_id = sw_id;
+		peer.peer_type = DRM_FABRIC_PEER_TYPE_SWITCH;
+		peer.port_index = i;
+		KUNIT_ASSERT_EQ(test,
+				drm_fabric_port_set_peer(leaf_ports[i], &peer), 0);
+	}
+
+	/* Switch peer IDs do not share the endpoint identity namespace. */
+	for (i = 0; i < SW_LEAVES; i++) {
+		KUNIT_EXPECT_TRUE(test, leaf_ports[i]->has_peer);
+		KUNIT_EXPECT_EQ(test, leaf_ports[i]->peer.peer_type,
+				(u32)DRM_FABRIC_PEER_TYPE_SWITCH);
+		KUNIT_EXPECT_EQ(test, leaf_ports[i]->peer.peer_id, sw_id);
+		KUNIT_EXPECT_EQ(test, leaf_ports[i]->peer.port_index, (u32)i);
+		KUNIT_EXPECT_NE(test, leaves[i]->fabric_ep_id, sw_id);
+	}
+#undef SW_LEAVES
+}
+
+enum fabrictest_shape { FT_SHAPE_LINEAR, FT_SHAPE_MESH };
+
+struct fabrictest_topo_param {
+	const char		*name;
+	enum fabrictest_shape	shape;
+	int			n_eps;
+	int			ports_per_ep;
+};
+
+static const struct fabrictest_topo_param fabrictest_topo_params[] = {
+	{ "linear-2", FT_SHAPE_LINEAR, 2, 1 },
+	{ "linear-3", FT_SHAPE_LINEAR, 3, 2 },
+	{ "linear-5", FT_SHAPE_LINEAR, 5, 2 },
+	{ "linear-8", FT_SHAPE_LINEAR, 8, 2 },
+	{ "mesh-2",   FT_SHAPE_MESH,   2, 1 },
+	{ "mesh-3",   FT_SHAPE_MESH,   3, 2 },
+	{ "mesh-4",   FT_SHAPE_MESH,   4, 3 },
+	{ "mesh-6",   FT_SHAPE_MESH,   6, 5 },
+};
+
+static void fabrictest_topo_desc(const struct fabrictest_topo_param *p,
+				 char *desc)
+{
+	strscpy(desc, p->name, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(fabrictest_topo, fabrictest_topo_params, fabrictest_topo_desc);
+
+#define FT_MAX_EPS	8
+#define FT_MAX_PORTS	16
+
+/* Count peered ports to validate each generated topology's degree. */
+static int fabrictest_peer_degree(struct drm_fabric_endpoint *ep, int n_ports)
+{
+	int j, deg = 0;
+
+	for (j = 0; j < n_ports; j++) {
+		struct drm_fabric_port *port = fabrictest_port(ep, j);
+
+		if (port && port->has_peer)
+			deg++;
+	}
+	return deg;
+}
+
+/*
+ * Chain eps[0]-eps[1]-...-eps[n-1]; endpoints get one link, interior nodes
+ * get two, each consuming the next free port.
+ */
+static void fabrictest_verify_linear_topology(struct kunit *test,
+					      const struct fabrictest_topo_param *p,
+					      struct drm_fabric_endpoint **eps)
+{
+	int cursor[FT_MAX_EPS] = {0};
+	struct drm_fabric_peer peer;
+	int i;
+
+	for (i = 0; i < p->n_eps - 1; i++) {
+		int ai = cursor[i]++;
+		int bi = cursor[i + 1]++;
+
+		peer = (struct drm_fabric_peer){
+			.peer_id = eps[i + 1]->fabric_ep_id,
+			.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+			.port_index = bi,
+		};
+		drm_fabric_port_set_peer(fabrictest_port(eps[i], ai), &peer);
+
+		peer = (struct drm_fabric_peer){
+			.peer_id = eps[i]->fabric_ep_id,
+			.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+			.port_index = ai,
+		};
+		drm_fabric_port_set_peer(fabrictest_port(eps[i + 1], bi), &peer);
+	}
+
+	for (i = 0; i < p->n_eps; i++) {
+		int want = (i == 0 || i == p->n_eps - 1) ? 1 : 2;
+
+		KUNIT_EXPECT_EQ_MSG(test,
+				    fabrictest_peer_degree(eps[i], p->ports_per_ep),
+				    want, "linear ep%d degree", i);
+	}
+}
+
+/*
+ * Fully-connected K_N: every endpoint peers with every other one.
+ */
+static void fabrictest_verify_mesh_topology(struct kunit *test,
+					    const struct fabrictest_topo_param *p,
+					    struct drm_fabric_endpoint **eps)
+{
+	struct drm_fabric_peer peer;
+	int i, j, k;
+
+	for (i = 0; i < p->n_eps; i++) {
+		int port_idx = 0;
+
+		for (j = 0; j < p->n_eps; j++) {
+			int peer_port = 0;
+
+			if (i == j)
+				continue;
+			for (k = 0; k < p->n_eps; k++) {
+				if (k == j)
+					continue;
+				if (k == i)
+					break;
+				peer_port++;
+			}
+			peer = (struct drm_fabric_peer){
+				.peer_id = eps[j]->fabric_ep_id,
+				.peer_type = DRM_FABRIC_PEER_TYPE_ACCEL,
+				.port_index = peer_port,
+			};
+			drm_fabric_port_set_peer(fabrictest_port(eps[i], port_idx),
+						 &peer);
+			port_idx++;
+		}
+	}
+
+	for (i = 0; i < p->n_eps; i++)
+		KUNIT_EXPECT_EQ_MSG(test,
+				    fabrictest_peer_degree(eps[i], p->ports_per_ep),
+				    p->n_eps - 1, "mesh ep%d degree", i);
+
+	/* Spot-check reciprocity; exhaustive K_4 coverage is tested separately. */
+	for (j = 0; j < p->n_eps - 1; j++) {
+		struct drm_fabric_port *port = fabrictest_port(eps[0], j);
+		struct drm_fabric_port *back;
+		int pi, peer_ep = -1;
+
+		KUNIT_ASSERT_NOT_NULL(test, port);
+		KUNIT_ASSERT_TRUE(test, port->has_peer);
+
+		for (pi = 0; pi < p->n_eps; pi++)
+			if (eps[pi]->fabric_ep_id == port->peer.peer_id) {
+				peer_ep = pi;
+				break;
+			}
+		KUNIT_EXPECT_GE(test, peer_ep, 0);
+		if (peer_ep < 0)
+			continue;
+		back = fabrictest_port(eps[peer_ep], port->peer.port_index);
+		KUNIT_ASSERT_NOT_NULL(test, back);
+		KUNIT_EXPECT_TRUE(test, back->has_peer);
+		KUNIT_EXPECT_EQ(test, back->peer.peer_id,
+				eps[0]->fabric_ep_id);
+	}
+}
+
+/*
+ * Parameterized over fabrictest_topo_params: linear chains and full meshes at
+ * several sizes, checked generically via fabrictest_peer_degree().
+ */
+static void drm_fabric_test_topology_param(struct kunit *test)
+{
+	const struct fabrictest_topo_param *p = test->param_value;
+	struct device *fabrictest_dev = fabrictest_alloc_dev(test);
+	struct drm_fabric_desc fdesc = {
+		.type = DRM_FABRIC_TYPE_SYNTHETIC, .name = "test-topo",
+	};
+	struct drm_fabric_port_desc pdescs[FT_MAX_PORTS];
+	struct drm_fabric_endpoint *eps[FT_MAX_EPS];
+	struct drm_fabric_endpoint_desc edesc;
+	struct drm_fabric *fab;
+	int i, j;
+
+	KUNIT_ASSERT_LE(test, p->n_eps, FT_MAX_EPS);
+	KUNIT_ASSERT_LE(test, p->ports_per_ep, FT_MAX_PORTS);
+
+	memset(pdescs, 0, sizeof(pdescs));
+	for (j = 0; j < p->ports_per_ep; j++) {
+		pdescs[j].index = j;
+		pdescs[j].max_lane_count = 4;
+		pdescs[j].max_lane_signaling_rate_mbps = 200000;
+	}
+
+	fab = drm_fabric_register(&fdesc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fab));
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, fabrictest_unregister_fabric, fab));
+
+	for (i = 0; i < p->n_eps; i++) {
+		memset(&edesc, 0, sizeof(edesc));
+		edesc.fabric_ep_id = 0x100 + i;
+		edesc.parent = fabrictest_dev;
+		edesc.ports = pdescs;
+		edesc.num_ports = p->ports_per_ep;
+
+		eps[i] = drm_fabric_endpoint_register(fab, &edesc);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(eps[i]));
+		KUNIT_ASSERT_EQ(test, 0,
+				kunit_add_action_or_reset(test,
+							  fabrictest_unregister_endpoint, eps[i]));
+	}
+
+	if (p->shape == FT_SHAPE_LINEAR)
+		fabrictest_verify_linear_topology(test, p, eps);
+	else
+		fabrictest_verify_mesh_topology(test, p, eps);
+}
+
+static struct kunit_case drm_fabric_test_cases[] = {
+	KUNIT_CASE(drm_fabric_test_fabric_register),
+	KUNIT_CASE(drm_fabric_test_unregister_reports_removal),
+	KUNIT_CASE(drm_fabric_test_instance_id_unique),
+	KUNIT_CASE(drm_fabric_test_endpoint_register),
+	KUNIT_CASE(drm_fabric_test_endpoint_requires_fabric),
+	KUNIT_CASE(drm_fabric_test_endpoint_requires_port_array),
+	KUNIT_CASE(drm_fabric_test_endpoint_register_stale_fabric),
+	KUNIT_CASE(drm_fabric_test_unregister_rejects_non_member),
+	KUNIT_CASE(drm_fabric_test_unregister_rejects_same_id),
+	KUNIT_CASE(drm_fabric_test_ep_membership_rejects_non_member),
+	KUNIT_CASE(drm_fabric_test_ep_membership_rejects_same_id),
+	KUNIT_CASE(drm_fabric_test_port_register_peer),
+	KUNIT_CASE(drm_fabric_test_base_seq_advances),
+	KUNIT_CASE(drm_fabric_test_generation_wrap_nonzero),
+	KUNIT_CASE(drm_fabric_test_port_state_transition),
+	KUNIT_CASE(drm_fabric_test_port_oper_state_rejects_invalid),
+	KUNIT_CASE(drm_fabric_test_register_rejects_invalid_type),
+	KUNIT_CASE(drm_fabric_test_mesh_kn_topology),
+	KUNIT_CASE(drm_fabric_test_port_stats_ops_registration),
+	KUNIT_CASE(drm_fabric_test_local_unplug_keeps_edge),
+	KUNIT_CASE(drm_fabric_test_remote_peer_retained),
+	KUNIT_CASE(drm_fabric_test_subtree_delete_single_bump),
+	KUNIT_CASE(drm_fabric_test_switch_topology),
+	KUNIT_CASE_PARAM(drm_fabric_test_topology_param,
+			 fabrictest_topo_gen_params),
+	{}
+};
+
+static struct kunit_suite drm_fabric_test_suite = {
+	.name = "drm_fabric",
+	.test_cases = drm_fabric_test_cases,
+};
+
+kunit_test_suite(drm_fabric_test_suite);
-- 
2.43.0


  parent reply	other threads:[~2026-08-24  8:10 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-24  8:09 [RFC PATCH 0/12] drm/fabric: vendor-neutral topology infrastructure for scale-up accelerator interconnects Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 01/12] drm/fabric: add core object model and provider API Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 02/12] drm/fabric: add query uAPI and generated headers Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 03/12] drm/fabric: implement query netlink operations Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 04/12] drm/fabric: add read-only synthetic provider Konstantin Sinyuk
2026-08-24  8:09 ` Konstantin Sinyuk [this message]
2026-08-24  8:09 ` [RFC PATCH 06/12] drm/fabric: add YNL query and policy selftests Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 07/12] drm/fabric: add topology-provisioning core Konstantin Sinyuk
2026-08-24  8:09 ` [RFC PATCH 08/12] drm/fabric: add provisioning netlink uAPI Konstantin Sinyuk

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=35955cefe73d0dd9d7553542ece17dfb65c04576.1787552412.git.ksinyuk@kernel.org \
    --to=ksinyuk@kernel.org \
    --cc=airlied@gmail.com \
    --cc=corbet@lwn.net \
    --cc=davem@davemloft.net \
    --cc=donald.hunter@gmail.com \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=edumazet@google.com \
    --cc=francois.dugast@intel.com \
    --cc=horms@kernel.org \
    --cc=ilia.levi@intel.com \
    --cc=kuba@kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=mripard@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=rodrigo.vivi@intel.com \
    --cc=simona@ffwll.ch \
    --cc=skhan@linuxfoundation.org \
    --cc=tzimmermann@suse.de \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox