From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8D1233A6F11; Mon, 24 Aug 2026 08:10:03 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787559007; cv=none; b=ShqiW0QhuLci8m6l3DgLpRFsCzMDr+m7ZMCroubjd8CL24759cl6SEvatWbfvSON+dwhfm4hFq9fTP08XyF/MwaAurNq0PepHROT2hjFyKAgrpzIHE1EY6KL/Q2U9mFcn7tsNVs1py1won8WQrEPyv4OhQnfrrxVMVONs1BK7MU= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787559007; c=relaxed/simple; bh=oLKFdXkaKhYXwFINoDtJ+2W0aIR4MnaZoVU8+U9z/9A=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=LbGgMmLZSqrj57l2vOhLJgpWIEyvE9BaGhS6c9n/TTibiho66S4zzeWRuBIP8xzdVMGNs7eoinMjf0tzOtjKBERunw01sp04BJLiM9yRpOZ2SHz8JsWu42X6JyHBupGLFkQS8kxGN5EsI/g+4oOAHljPzGY/jGA9PLv4XQyPxT0= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=NB9U7mhf; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="NB9U7mhf" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 2F3241F00A3D; Mon, 24 Aug 2026 08:10:02 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1787559003; bh=BiYaJc62EZhm/NBjwSmyTfIKonOmTKX6EXkPb11jF98=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=NB9U7mhfryZQDlW69zKhteQeE1x4Ldn5y1UU15P7uln0MF4JAcBg5IgT8ibQzHLiW Ysp5lvHDPD06EXEVUCfKid4954c2/ABa0qJ5f+koNnahG0OXyx8YfFGsphjDs2Mibz QQytwVIZxiep8ixbUPnJwwpTHvIjzmYO8iBwNUaXlJAOK5h0hwOff3vrlrQ41LX71t KE5Z21MgXT4JidHOdjYo1ytYtGk1cF1rGNs2c/1PgeoDSib0CTfzBFADUCdgtLiho5 jMUzd6TQMhJwEnuEIVcAI5xHKpgv1oOBwi9hnRXcSdHZFc5qvVxRkJHCpCWDlwMggm uexZJ/LzJPAIg== From: Konstantin Sinyuk To: dri-devel@lists.freedesktop.org Cc: Maarten Lankhorst , Francois Dugast , David Airlie , Simona Vetter , Maxime Ripard , Thomas Zimmermann , Jonathan Corbet , Shuah Khan , Donald Hunter , Jakub Kicinski , "David S. Miller" , Eric Dumazet , Paolo Abeni , Simon Horman , Ilia Levi , Rodrigo Vivi , 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 Message-ID: <35955cefe73d0dd9d7553542ece17dfb65c04576.1787552412.git.ksinyuk@kernel.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 +#include + +#include +#include +#include +#include + +#include +#include + +#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