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
next prev 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