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 <[email protected]>
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

Reply via email to