Linux Documentation
 help / color / mirror / Atom feed
* [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver
@ 2026-11-05 15:18 Petr Oros
  2026-11-05 15:18 ` [PATCH net-next v2 01/14] dpll: dpllsim: Add " Petr Oros
                   ` (14 more replies)
  0 siblings, 15 replies; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

The DPLL subsystem has no in-tree driver that can run without real
hardware, so core changes and userspace tooling are hard to exercise in
CI, and the pin-on-pin and reference sync paths see almost no coverage
outside the vendors' labs. This series adds dpllsim, a simulated DPLL
device driver, together with selftests that run against it.

The driver is controlled through configfs, following gpio-sim. Writing a
clock ID into a device directory and setting "live" to 1 registers a
DPLL device with the core. Pins are directories under pins/ with their
type, frequency, direction and priority as attributes, and all of them
are registered when the device goes live. The split between creating
the configuration and going live keeps the register and unregister
paths reachable from userspace, so they can be exercised repeatedly
without reloading the module. Nothing in the simulator is a stable ABI.

The patches follow the shape of the zl3073x introduction. The
driver starts with the device alone, then gains pins and the static pin
operations one at a time (state, priority, frequency, phase adjustment),
and every feature patch is followed by the selftest that covers it. The
last two patches add concurrency stress tests and reference tracker
leak tests for the lifecycle paths.

The simulation loop (lock state machine, phase and frequency
measurements, monitoring features), the topology features (MUX pin
hierarchies, reference sync, media failure injection), netdevsim
integration and device mode setting follow in two further series once
this one is in, to stay within the 15 patch limit.

The driver and the selftests were developed with an LLM coding
assistant (Claude Code) working under my direction. I chose the
configfs design, the split into one feature per patch and the test
scenarios, and I reviewed every line. The assistant wrote most of the
code and the first drafts of the changelogs, and it found and fixed
bugs by running the suite on the lab host. The series then went
through several rounds of an LLM based patch reviewer whose findings
were fixed, and through a mutation test in which deliberately broken
driver and core code was caught by the tests.

The selftests live in tools/testing/selftests/drivers/net/dpll and run
with "make -C tools/testing/selftests TARGETS=drivers/net/dpll
run_tests". By default they create dpllsim devices; with
DPLL_DEV_ID=<id> the basic and feature scripts run against an existing
device instead, so the same suite exercises real hardware. The whole
suite was run through the kselftest runner on an ice E810 host with
CONFIG_KASAN, CONFIG_PROVE_LOCKING and CONFIG_DPLL_REFCNT_TRACKER
enabled, against the simulator and, with DPLL_DEV_ID, against the ice
DPLL, where the tests that need a simulator skip and the pin state and
priorities of the hardware are left as they were. All pass with no
warnings in dmesg.

---
v2:
- sort tools/testing/selftests/drivers/net/dpll/config alphabetically
  and make the scripts clean under the shellcheck run of the netdev CI
  (quoted exit and return values, shellcheck directives for the globals
  shared between the library and the scripts and for the optional
  argument of dpllsim_setup_standard_device())

v1: https://lore.kernel.org/all/20261103093910.596225-1-poros@redhat.com/
---
Petr Oros (14):
  dpll: dpllsim: Add simulated DPLL device driver
  selftests: dpll: add test library and device lifecycle tests
  dpll: dpllsim: Add configurable pins
  selftests: dpll: add pin enumeration and property tests
  dpll: dpllsim: Add support to set input pin state
  selftests: dpll: add pin state tests
  dpll: dpllsim: Add support to get/set priority on input pins
  selftests: dpll: add pin priority test
  dpll: dpllsim: Add support to set frequency on pins
  selftests: dpll: add pin frequency test
  dpll: dpllsim: Add support to adjust phase
  selftests: dpll: add pin phase adjust test
  selftests: dpll: add concurrency stress tests
  selftests: dpll: add reference leak tests

 Documentation/driver-api/dpllsim.rst          |  72 ++
 Documentation/driver-api/index.rst            |   1 +
 MAINTAINERS                                   |   9 +
 drivers/dpll/Kconfig                          |   1 +
 drivers/dpll/Makefile                         |   1 +
 drivers/dpll/dpllsim/Kconfig                  |  16 +
 drivers/dpll/dpllsim/Makefile                 |   4 +
 drivers/dpll/dpllsim/core.c                   | 471 ++++++++++
 drivers/dpll/dpllsim/dpll.c                   | 475 ++++++++++
 drivers/dpll/dpllsim/dpllsim.h                |  99 ++
 tools/testing/selftests/Makefile              |   1 +
 .../selftests/drivers/net/dpll/Makefile       |  16 +
 .../testing/selftests/drivers/net/dpll/config |   6 +
 .../selftests/drivers/net/dpll/dpll_basic.sh  | 221 +++++
 .../drivers/net/dpll/dpll_features.sh         | 397 ++++++++
 .../drivers/net/dpll/dpll_reftrack.sh         | 184 ++++
 .../selftests/drivers/net/dpll/dpll_stress.sh | 562 ++++++++++++
 .../selftests/drivers/net/dpll/settings       |   1 +
 .../selftests/drivers/net/lib/sh/dpll_lib.sh  | 850 ++++++++++++++++++
 19 files changed, 3387 insertions(+)
 create mode 100644 Documentation/driver-api/dpllsim.rst
 create mode 100644 drivers/dpll/dpllsim/Kconfig
 create mode 100644 drivers/dpll/dpllsim/Makefile
 create mode 100644 drivers/dpll/dpllsim/core.c
 create mode 100644 drivers/dpll/dpllsim/dpll.c
 create mode 100644 drivers/dpll/dpllsim/dpllsim.h
 create mode 100644 tools/testing/selftests/drivers/net/dpll/Makefile
 create mode 100644 tools/testing/selftests/drivers/net/dpll/config
 create mode 100755 tools/testing/selftests/drivers/net/dpll/dpll_basic.sh
 create mode 100755 tools/testing/selftests/drivers/net/dpll/dpll_features.sh
 create mode 100755 tools/testing/selftests/drivers/net/dpll/dpll_reftrack.sh
 create mode 100755 tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
 create mode 100644 tools/testing/selftests/drivers/net/dpll/settings
 create mode 100644 tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh

-- 
2.56.0


^ permalink raw reply	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 01/14] dpll: dpllsim: Add simulated DPLL device driver
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:47   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
                   ` (13 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Add dpllsim, a driver providing simulated DPLL devices for testing the
DPLL subsystem and its userspace tooling without real hardware, in the
same spirit as netdevsim for network devices. The DPLL core identifies
a device by clock ID, index and module only, so the driver needs no bus
and no struct device at all.

Instances are created and configured at runtime through configfs,
following the gpio-sim precedent for objects that userspace creates on
demand. Creating a directory under /sys/kernel/config/dpllsim/ allocates
an instance, its clock_id attribute selects the clock ID, writing 1 to
live registers the DPLL device with the core and writing 0 unregisters
it again. Removing the directory tears a live instance down implicitly.
Keeping registration separate from creation lets a test register and
unregister the same device repeatedly, which is where the core bugs the
driver is meant to catch tend to live, and it allows the structural
attributes to be refused with EBUSY while the device is visible to
userspace. Unlike sysfs, configfs does not imply a stable ABI, which
suits a driver whose knobs exist only to drive tests.

Reading live reports whether the device is registered with the DPLL
subsystem, it turns 1 only once the whole live transition succeeded and
0 again as the first step of the teardown. The configuration is frozen
earlier, from the moment the transition obtains the device handle, so
that a configuration write racing with the transition is refused rather
than lost.

The device registers as a PPS type DPLL with the two mandatory callbacks
only. mode_get reports automatic mode and lock_status_get reports
unlocked with no error. Pins come with the following patches and the
simulation of lock acquisition with a later series.

Document the configfs layout under Documentation/driver-api and add a
MAINTAINERS entry.

Signed-off-by: Petr Oros <poros@redhat.com>
---
 Documentation/driver-api/dpllsim.rst |  41 +++++
 Documentation/driver-api/index.rst   |   1 +
 MAINTAINERS                          |   7 +
 drivers/dpll/Kconfig                 |   1 +
 drivers/dpll/Makefile                |   1 +
 drivers/dpll/dpllsim/Kconfig         |  16 ++
 drivers/dpll/dpllsim/Makefile        |   4 +
 drivers/dpll/dpllsim/core.c          | 221 +++++++++++++++++++++++++++
 drivers/dpll/dpllsim/dpll.c          |  84 ++++++++++
 drivers/dpll/dpllsim/dpllsim.h       |  58 +++++++
 10 files changed, 434 insertions(+)
 create mode 100644 Documentation/driver-api/dpllsim.rst
 create mode 100644 drivers/dpll/dpllsim/Kconfig
 create mode 100644 drivers/dpll/dpllsim/Makefile
 create mode 100644 drivers/dpll/dpllsim/core.c
 create mode 100644 drivers/dpll/dpllsim/dpll.c
 create mode 100644 drivers/dpll/dpllsim/dpllsim.h

diff --git a/Documentation/driver-api/dpllsim.rst b/Documentation/driver-api/dpllsim.rst
new file mode 100644
index 00000000000000..86a5d523534055
--- /dev/null
+++ b/Documentation/driver-api/dpllsim.rst
@@ -0,0 +1,41 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+==========================
+DPLL simulator (dpllsim)
+==========================
+
+The dpllsim driver provides simulated DPLL devices for testing the DPLL
+subsystem and its userspace tooling without real hardware, similar to
+what netdevsim provides for network devices.
+
+Configuration interface
+=======================
+
+Devices are described in configfs under ``/sys/kernel/config/dpllsim/``.
+A directory created there is a device instance. Attributes are written
+while the instance is not live, then ``live`` is set to 1 to register
+the device with the DPLL subsystem. Writing 0 unregisters it again.
+Structural attributes return ``EBUSY`` while the instance is live.
+
+Device directory ``/sys/kernel/config/dpllsim/<dev>/``:
+
+=======================  =====================================================
+``clock_id``             Clock ID of the DPLL device, must be nonzero and
+                         unique among live instances
+``live``                 1 registers the device, 0 unregisters it
+=======================  =====================================================
+
+Example
+=======
+
+Create a device and make it live::
+
+    cd /sys/kernel/config/dpllsim
+    mkdir dev0 && cd dev0
+    echo 100 > clock_id
+    echo 1 > live
+
+    dpll device show
+
+To remove the device, write 0 to ``live`` and remove the device
+directory.
diff --git a/Documentation/driver-api/index.rst b/Documentation/driver-api/index.rst
index aaf5d76ed074ed..e2271889e33400 100644
--- a/Documentation/driver-api/index.rst
+++ b/Documentation/driver-api/index.rst
@@ -87,6 +87,7 @@ Subsystem-specific APIs
    crypto/index
    dmaengine/index
    dpll
+   dpllsim
    edac
    extcon
    firmware/index
diff --git a/MAINTAINERS b/MAINTAINERS
index 09713dc4c6a77d..147a5466902bc7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8070,6 +8070,13 @@ F:	Documentation/networking/device_drivers/ethernet/freescale/dpaa2/switch-drive
 F:	drivers/net/ethernet/freescale/dpaa2/dpaa2-switch*
 F:	drivers/net/ethernet/freescale/dpaa2/dpsw*
 
+DPLL SIMULATOR DRIVER (dpllsim)
+M:	Petr Oros <poros@redhat.com>
+L:	netdev@vger.kernel.org
+S:	Maintained
+F:	Documentation/driver-api/dpllsim.rst
+F:	drivers/dpll/dpllsim/
+
 DPLL SUBSYSTEM
 M:	Vadim Fedorenko <vadim.fedorenko@linux.dev>
 M:	Arkadiusz Kubalewski <arkadiusz.kubalewski@intel.com>
diff --git a/drivers/dpll/Kconfig b/drivers/dpll/Kconfig
index be98969f040ab6..c6bd5392ce8753 100644
--- a/drivers/dpll/Kconfig
+++ b/drivers/dpll/Kconfig
@@ -24,5 +24,6 @@ config DPLL_REFCNT_TRACKER
 	  If unsure, say N.
 
 source "drivers/dpll/zl3073x/Kconfig"
+source "drivers/dpll/dpllsim/Kconfig"
 
 endmenu
diff --git a/drivers/dpll/Makefile b/drivers/dpll/Makefile
index 9e7a3a3e592e81..1ee1daa3a46cc7 100644
--- a/drivers/dpll/Makefile
+++ b/drivers/dpll/Makefile
@@ -9,3 +9,4 @@ dpll-y                  += dpll_netlink.o
 dpll-y                  += dpll_nl.o
 
 obj-$(CONFIG_ZL3073X)	+= zl3073x/
+obj-$(CONFIG_DPLLSIM)	+= dpllsim/
diff --git a/drivers/dpll/dpllsim/Kconfig b/drivers/dpll/dpllsim/Kconfig
new file mode 100644
index 00000000000000..45ed93ba3dee5a
--- /dev/null
+++ b/drivers/dpll/dpllsim/Kconfig
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+config DPLLSIM
+	tristate "DPLL simulator driver"
+	depends on NET
+	select DPLL
+	select CONFIGFS_FS
+	help
+	  This driver provides simulated DPLL devices for testing and
+	  development of the DPLL subsystem and its userspace tooling
+	  without real hardware. Devices with arbitrary pin topologies
+	  are created and configured at runtime through configfs,
+	  similar to what gpio-sim provides for GPIO.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called dpllsim.
diff --git a/drivers/dpll/dpllsim/Makefile b/drivers/dpll/dpllsim/Makefile
new file mode 100644
index 00000000000000..905df2ee00874f
--- /dev/null
+++ b/drivers/dpll/dpllsim/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+obj-$(CONFIG_DPLLSIM) += dpllsim.o
+dpllsim-y := core.o dpll.o
diff --git a/drivers/dpll/dpllsim/core.c b/drivers/dpll/dpllsim/core.c
new file mode 100644
index 00000000000000..0772f409d013b7
--- /dev/null
+++ b/drivers/dpll/dpllsim/core.c
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * DPLL simulator - configfs control plane
+ * Copyright (c) 2026 Red Hat, Inc.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/cleanup.h>
+#include <linux/configfs.h>
+#include <linux/kstrtox.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/sysfs.h>
+
+#include "dpllsim.h"
+
+static struct configfs_subsystem dpllsim_subsys;
+
+static struct dpllsim_dev *to_dpllsim_dev(struct config_item *item)
+{
+	return container_of(to_config_group(item), struct dpllsim_dev, group);
+}
+
+/* The clock id is checked at live=1, so both share the subsystem mutex */
+static ssize_t dpllsim_dev_clock_id_show(struct config_item *item, char *page)
+{
+	struct dpllsim_dev *sim = to_dpllsim_dev(item);
+
+	guard(mutex)(&dpllsim_subsys.su_mutex);
+	return sysfs_emit(page, "%llu\n", sim->clock_id);
+}
+
+static ssize_t dpllsim_dev_clock_id_store(struct config_item *item,
+					  const char *page, size_t count)
+{
+	struct dpllsim_dev *sim = to_dpllsim_dev(item);
+	u64 val;
+	int err;
+
+	err = kstrtou64(page, 10, &val);
+	if (err)
+		return err;
+
+	guard(mutex)(&dpllsim_subsys.su_mutex);
+	if (dpllsim_dev_frozen(sim))
+		return -EBUSY;
+	sim->clock_id = val;
+	return count;
+}
+CONFIGFS_ATTR(dpllsim_dev_, clock_id);
+
+static ssize_t dpllsim_dev_live_show(struct config_item *item, char *page)
+{
+	struct dpllsim_dev *sim = to_dpllsim_dev(item);
+
+	guard(mutex)(&sim->lock);
+	return sysfs_emit(page, "%d\n", dpllsim_dev_live(sim));
+}
+
+/*
+ * Two live instances must not share a clock id, dpll_device_get() would
+ * hand out the same dpll_device to both of them instead of registering
+ * a new one.
+ */
+static bool dpllsim_clock_id_in_use(struct dpllsim_dev *sim)
+{
+	struct config_item *item;
+
+	lockdep_assert_held(&dpllsim_subsys.su_mutex);
+
+	list_for_each_entry(item, &dpllsim_subsys.su_group.cg_children,
+			    ci_entry) {
+		struct dpllsim_dev *other = to_dpllsim_dev(item);
+
+		if (other != sim && dpllsim_dev_live(other) &&
+		    other->clock_id == sim->clock_id)
+			return true;
+	}
+
+	return false;
+}
+
+/*
+ * The subsystem mutex excludes the live transitions of the other
+ * instances for the whole deploy sequence.
+ */
+static ssize_t dpllsim_dev_live_store(struct config_item *item,
+				      const char *page, size_t count)
+{
+	struct dpllsim_dev *sim = to_dpllsim_dev(item);
+	bool live;
+	int err;
+
+	err = kstrtobool(page, &live);
+	if (err)
+		return err;
+
+	guard(mutex)(&dpllsim_subsys.su_mutex);
+
+	if (live == dpllsim_dev_live(sim))
+		return count;
+
+	if (!live) {
+		dpllsim_dev_undeploy(sim);
+		return count;
+	}
+
+	if (!sim->clock_id)
+		return -EINVAL;
+	if (dpllsim_clock_id_in_use(sim))
+		return -EEXIST;
+
+	err = dpllsim_dev_deploy(sim);
+	if (err)
+		return err;
+
+	return count;
+}
+CONFIGFS_ATTR(dpllsim_dev_, live);
+
+static struct configfs_attribute *dpllsim_dev_attrs[] = {
+	&dpllsim_dev_attr_clock_id,
+	&dpllsim_dev_attr_live,
+	NULL,
+};
+
+static void dpllsim_dev_release(struct config_item *item)
+{
+	struct dpllsim_dev *sim = to_dpllsim_dev(item);
+
+	mutex_destroy(&sim->lock);
+	kfree(sim);
+}
+
+static const struct configfs_item_operations dpllsim_dev_item_ops = {
+	.release	= dpllsim_dev_release,
+};
+
+static const struct config_item_type dpllsim_dev_type = {
+	.ct_item_ops	= &dpllsim_dev_item_ops,
+	.ct_attrs	= dpllsim_dev_attrs,
+	.ct_owner	= THIS_MODULE,
+};
+
+static struct config_group *dpllsim_make_group(struct config_group *group,
+					       const char *name)
+{
+	struct dpllsim_dev *sim;
+
+	sim = kzalloc_obj(*sim);
+	if (!sim)
+		return ERR_PTR(-ENOMEM);
+
+	mutex_init(&sim->lock);
+	sim->mode = DPLL_MODE_AUTOMATIC;
+	sim->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
+	sim->lock_status_error = DPLL_LOCK_STATUS_ERROR_NONE;
+
+	config_group_init_type_name(&sim->group, name, &dpllsim_dev_type);
+
+	return &sim->group;
+}
+
+/* Runs under the subsystem mutex, unlike release */
+static void dpllsim_drop_item(struct config_group *group,
+			      struct config_item *item)
+{
+	struct dpllsim_dev *sim = to_dpllsim_dev(item);
+
+	if (dpllsim_dev_live(sim))
+		dpllsim_dev_undeploy(sim);
+	config_item_put(item);
+}
+
+static const struct configfs_group_operations dpllsim_group_ops = {
+	.make_group	= dpllsim_make_group,
+	.drop_item	= dpllsim_drop_item,
+};
+
+static const struct config_item_type dpllsim_type = {
+	.ct_group_ops	= &dpllsim_group_ops,
+	.ct_owner	= THIS_MODULE,
+};
+
+static struct configfs_subsystem dpllsim_subsys = {
+	.su_group = {
+		.cg_item = {
+			.ci_namebuf	= "dpllsim",
+			.ci_type	= &dpllsim_type,
+		},
+	},
+};
+
+static int __init dpllsim_init(void)
+{
+	int err;
+
+	config_group_init(&dpllsim_subsys.su_group);
+	mutex_init(&dpllsim_subsys.su_mutex);
+	err = configfs_register_subsystem(&dpllsim_subsys);
+	if (err) {
+		pr_err("failed to register the configfs subsystem: %d\n", err);
+		mutex_destroy(&dpllsim_subsys.su_mutex);
+	}
+
+	return err;
+}
+module_init(dpllsim_init);
+
+static void __exit dpllsim_exit(void)
+{
+	configfs_unregister_subsystem(&dpllsim_subsys);
+	mutex_destroy(&dpllsim_subsys.su_mutex);
+}
+module_exit(dpllsim_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Simulated DPLL device driver");
+MODULE_AUTHOR("Petr Oros <poros@redhat.com>");
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
new file mode 100644
index 00000000000000..ae2c8fcf238c68
--- /dev/null
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * DPLL simulator - DPLL subsystem interface
+ * Copyright (c) 2026 Red Hat, Inc.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/dpll.h>
+#include <linux/module.h>
+
+#include "dpllsim.h"
+
+static int dpllsim_mode_get(const struct dpll_device *dpll, void *priv,
+			    enum dpll_mode *mode,
+			    struct netlink_ext_ack *extack)
+{
+	struct dpllsim_dev *sim = priv;
+
+	guard(mutex)(&sim->lock);
+	*mode = sim->mode;
+	return 0;
+}
+
+static int dpllsim_lock_status_get(const struct dpll_device *dpll, void *priv,
+				   enum dpll_lock_status *status,
+				   enum dpll_lock_status_error *status_error,
+				   struct netlink_ext_ack *extack)
+{
+	struct dpllsim_dev *sim = priv;
+
+	guard(mutex)(&sim->lock);
+	*status = sim->lock_status;
+	*status_error = sim->lock_status_error;
+	return 0;
+}
+
+static const struct dpll_device_ops dpllsim_dpll_ops = {
+	.mode_get = dpllsim_mode_get,
+	.lock_status_get = dpllsim_lock_status_get,
+};
+
+int dpllsim_dev_deploy(struct dpllsim_dev *sim)
+{
+	struct dpll_device *dpll;
+	int err;
+
+	dpll = dpll_device_get(sim->clock_id, 0, THIS_MODULE, &sim->tracker);
+	if (IS_ERR(dpll))
+		return PTR_ERR(dpll);
+
+	/* From here on the configuration is frozen */
+	mutex_lock(&sim->lock);
+	sim->dpll = dpll;
+	mutex_unlock(&sim->lock);
+
+	err = dpll_device_register(sim->dpll, DPLL_TYPE_PPS,
+				   &dpllsim_dpll_ops, sim);
+	if (err)
+		goto err_put_dpll;
+
+	mutex_lock(&sim->lock);
+	sim->registered = true;
+	mutex_unlock(&sim->lock);
+
+	return 0;
+
+err_put_dpll:
+	dpll_device_put(sim->dpll, &sim->tracker);
+	mutex_lock(&sim->lock);
+	sim->dpll = NULL;
+	mutex_unlock(&sim->lock);
+	return err;
+}
+
+void dpllsim_dev_undeploy(struct dpllsim_dev *sim)
+{
+	scoped_guard(mutex, &sim->lock)
+		sim->registered = false;
+
+	dpll_device_unregister(sim->dpll, &dpllsim_dpll_ops, sim);
+	dpll_device_put(sim->dpll, &sim->tracker);
+	scoped_guard(mutex, &sim->lock)
+		sim->dpll = NULL;
+}
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
new file mode 100644
index 00000000000000..5667f8827c98c1
--- /dev/null
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * DPLL simulator - internal header
+ * Copyright (c) 2026 Red Hat, Inc.
+ */
+
+#ifndef _DPLLSIM_H
+#define _DPLLSIM_H
+
+#include <linux/configfs.h>
+#include <linux/dpll.h>
+#include <linux/mutex.h>
+
+struct dpllsim_dev {
+	struct config_group group;
+
+	/*
+	 * Set from the start of a live transition until the device is
+	 * undeployed, written under both the subsystem mutex and @lock.
+	 * Freezes the configuration, see dpllsim_dev_frozen().
+	 */
+	struct dpll_device *dpll;
+	dpll_tracker tracker;
+	/*
+	 * True while the device is registered with the DPLL subsystem,
+	 * written under both the subsystem mutex and @lock. This is what
+	 * the live attribute reports, see dpllsim_dev_live().
+	 */
+	bool registered;
+
+	/* Guarded by the subsystem mutex */
+	u64 clock_id;
+
+	/* Simulated state, guarded by @lock */
+	enum dpll_mode mode;
+	enum dpll_lock_status lock_status;
+	enum dpll_lock_status_error lock_status_error;
+
+	/* Protects the simulated state */
+	struct mutex lock;
+};
+
+/* Configuration changes are refused from the start of a live transition */
+static inline bool dpllsim_dev_frozen(const struct dpllsim_dev *sim)
+{
+	return !!sim->dpll;
+}
+
+/* Live means registered with the DPLL subsystem */
+static inline bool dpllsim_dev_live(const struct dpllsim_dev *sim)
+{
+	return sim->registered;
+}
+
+int dpllsim_dev_deploy(struct dpllsim_dev *sim);
+void dpllsim_dev_undeploy(struct dpllsim_dev *sim);
+
+#endif /* _DPLLSIM_H */
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 02/14] selftests: dpll: add test library and device lifecycle tests
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
  2026-11-05 15:18 ` [PATCH net-next v2 01/14] dpll: dpllsim: Add " Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:48   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 03/14] dpll: dpllsim: Add configurable pins Petr Oros
                   ` (12 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Add a shell library for DPLL selftests with wrappers around the iproute2
dpll tool and around the dpllsim configfs interface, and the first two
test scripts built on it. The library resolves devices by the clock ID
chosen at creation time, since netlink ids depend on what else exists
on the system, and the dpllsim helpers restrict the lookup to their
module, so that a device of another driver with the same clock ID is
never picked up. Every query runs the tool first and jq on what it
printed, so that a failed query is told apart from a missing attribute.
The library mounts configfs when nothing else did, and the exit trap of
each script undoes that, removes the devices the script created and
runs on INT and TERM as well, so that a timeout imposed by the harness
still cleans up.

Two runs of scripts that create dpllsim devices would share the one
dpllsim directory and could draw the same clock IDs, so every such
script takes a lock before it mounts or creates anything and skips when
another run holds it. The lock file lives under /run rather than in
/tmp, since the scripts run as root and open the file for writing, and
a symlink planted in the world-writable /tmp would redirect that open.

dpll_basic.sh checks that a device reports the mandatory attributes
over netlink. It runs against the device named by DPLL_DEV_ID or the
first device that is not a dpllsim one, and creates a dpllsim device
when there is none, so the same script serves the simulator and real
drivers. dpll_stress.sh is simulator only and exercises the lifecycle
paths of the core by creating and deleting devices in a loop and by
cycling one device through repeated register and unregister
transitions. The dpllsim MAINTAINERS entry covers the new directory
and the shared library.

Signed-off-by: Petr Oros <poros@redhat.com>
---
v2:
- sort the config file alphabetically (netdev CI check_selftest)
- quote the ksft_skip and ret expansions and add shellcheck directives
  for the globals read by the scripts, for ksft_skip, which comes from
  net/lib.sh, and for the optional argument of
  dpllsim_setup_standard_device() (netdev CI shellcheck)

v1: https://lore.kernel.org/all/20261103093910.596225-3-poros@redhat.com/
---
 MAINTAINERS                                   |   2 +
 tools/testing/selftests/Makefile              |   1 +
 .../selftests/drivers/net/dpll/Makefile       |  14 +
 .../testing/selftests/drivers/net/dpll/config |   3 +
 .../selftests/drivers/net/dpll/dpll_basic.sh  |  93 +++++
 .../selftests/drivers/net/dpll/dpll_stress.sh | 164 +++++++++
 .../selftests/drivers/net/dpll/settings       |   1 +
 .../selftests/drivers/net/lib/sh/dpll_lib.sh  | 334 ++++++++++++++++++
 8 files changed, 612 insertions(+)
 create mode 100644 tools/testing/selftests/drivers/net/dpll/Makefile
 create mode 100644 tools/testing/selftests/drivers/net/dpll/config
 create mode 100755 tools/testing/selftests/drivers/net/dpll/dpll_basic.sh
 create mode 100755 tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
 create mode 100644 tools/testing/selftests/drivers/net/dpll/settings
 create mode 100644 tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh

diff --git a/MAINTAINERS b/MAINTAINERS
index 147a5466902bc7..d4e0a7e97d3808 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8076,6 +8076,8 @@ L:	netdev@vger.kernel.org
 S:	Maintained
 F:	Documentation/driver-api/dpllsim.rst
 F:	drivers/dpll/dpllsim/
+F:	tools/testing/selftests/drivers/net/dpll/
+F:	tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
 
 DPLL SUBSYSTEM
 M:	Vadim Fedorenko <vadim.fedorenko@linux.dev>
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 6c77beba0956ef..d8991f43c70e46 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -24,6 +24,7 @@ TARGETS += drivers/ntsync
 TARGETS += drivers/s390x/uvdevice
 TARGETS += drivers/net
 TARGETS += drivers/net/bonding
+TARGETS += drivers/net/dpll
 TARGETS += drivers/net/netconsole
 TARGETS += drivers/net/team
 TARGETS += drivers/net/virtio_net
diff --git a/tools/testing/selftests/drivers/net/dpll/Makefile b/tools/testing/selftests/drivers/net/dpll/Makefile
new file mode 100644
index 00000000000000..7752fb518b6e9b
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/dpll/Makefile
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_INCLUDES := \
+	$(wildcard ../../../net/lib/sh/*.sh) \
+	../lib/sh/dpll_lib.sh \
+	../../../net/lib.sh \
+# end of TEST_INCLUDES
+
+TEST_PROGS := \
+	dpll_basic.sh \
+	dpll_stress.sh \
+# end of TEST_PROGS
+
+include ../../../lib.mk
diff --git a/tools/testing/selftests/drivers/net/dpll/config b/tools/testing/selftests/drivers/net/dpll/config
new file mode 100644
index 00000000000000..e2c90b6970bd43
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/dpll/config
@@ -0,0 +1,3 @@
+CONFIG_CONFIGFS_FS=y
+CONFIG_DPLL=y
+CONFIG_DPLLSIM=m
diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_basic.sh b/tools/testing/selftests/drivers/net/dpll/dpll_basic.sh
new file mode 100755
index 00000000000000..0d823fbc943a84
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_basic.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Basic DPLL subsystem test. Read-only validation of the device properties
+# exposed over netlink. Runs against a hardware DPLL device when one is
+# present (or when DPLL_DEV_ID is set), otherwise against a freshly
+# created dpllsim device.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../../net/lib.sh"
+source "$SCRIPTDIR/../lib/sh/dpll_lib.sh"
+
+ALL_TESTS="
+	test_device_properties
+"
+
+test_device_properties()
+{
+	local json
+
+	RET=0
+
+	# A device that cannot be queried at all would be reported as one
+	# that misses every attribute, so bail out on that first.
+	if ! json=$(dpll_device_json "$DPLL_DEV_ID") || [ -z "$json" ]; then
+		check_err 1 "device query failed"
+		log_test "DPLL device properties"
+		return
+	fi
+
+	local field
+	for field in '.id' '.["module-name"]' '.["clock-id"]' '.type' \
+		     '.mode' '.["lock-status"]'; do
+		if ! jq -e "$field" <<< "$json" &> /dev/null; then
+			check_err 1 "device is missing the $field attribute"
+		fi
+	done
+
+	log_test "DPLL device properties"
+}
+
+##############################################################################
+
+cleanup()
+{
+	# A second signal must not abort the teardown half way.
+	trap '' INT TERM
+
+	if [ -n "$CREATED_DEVICE" ]; then
+		dpllsim_delete_device "$CREATED_DEVICE" ||
+			log_info "cannot delete device $CREATED_DEVICE"
+	fi
+	# The device whose creation a signal interrupted is recorded by the
+	# library alone, a double delete of a tracked one is harmless.
+	if [ -n "$DPLLSIM_CREATED_NAME" ]; then
+		dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
+			log_info "cannot delete device $DPLLSIM_CREATED_NAME"
+	fi
+	dpll_lib_cleanup
+}
+
+dpll_lib_check_prereqs
+# Another run would find the devices of this one left behind, see the
+# lock in the library.
+dpll_lib_lock
+dpll_lib_init
+
+CREATED_DEVICE=""
+# The module check may mount configfs, so the exit trap that unmounts it
+# again has to be in place before. INT and TERM exit through it as well,
+# so that a timeout imposed by the harness still removes the device.
+trap cleanup EXIT
+trap 'exit 1' INT TERM
+
+if [ -z "$DPLL_DEV_ID" ]; then
+	dpllsim_check_module
+	if ! dpllsim_setup_standard_device; then
+		echo "SKIP: cannot create dpllsim device"
+		exit "$ksft_skip"
+	fi
+	CREATED_DEVICE=$DPLLSIM_CREATED_NAME
+	DPLL_DEV_ID=$DPLLSIM_DEV_ID
+	DPLL_MODULE_NAME="dpllsim"
+fi
+
+log_info "testing DPLL device $DPLL_DEV_ID (module $DPLL_MODULE_NAME)"
+
+tests_run
+
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
new file mode 100755
index 00000000000000..74bb6e8b3a8ed4
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
@@ -0,0 +1,164 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# DPLL subsystem stress test. Uses dpllsim to exercise rapid device
+# registration/unregistration, concurrent operations from multiple
+# processes, pin state change flooding and notification load, then
+# verifies via ref_tracker that no references leaked.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../../net/lib.sh"
+source "$SCRIPTDIR/../lib/sh/dpll_lib.sh"
+
+ALL_TESTS="
+	test_rapid_create_delete
+	test_deploy_cycle
+"
+
+STRESS_ITERATIONS=${STRESS_ITERATIONS:-20}
+
+CREATED_DEVICES=()
+
+track_device()
+{
+	CREATED_DEVICES+=("$1")
+}
+
+untrack_device()
+{
+	local name=$1 dev
+	local remaining=()
+
+	for dev in "${CREATED_DEVICES[@]}"; do
+		[ "$dev" = "$name" ] || remaining+=("$dev")
+	done
+	CREATED_DEVICES=("${remaining[@]}")
+}
+
+cleanup()
+{
+	local dev
+
+	# A second signal must not abort the teardown half way.
+	trap '' INT TERM
+
+	for dev in "${CREATED_DEVICES[@]}"; do
+		dpllsim_delete_device "$dev" ||
+			log_info "cannot delete device $dev"
+	done
+	# The device whose creation a signal interrupted is recorded by the
+	# library alone, a double delete of a tracked one is harmless.
+	if [ -n "$DPLLSIM_CREATED_NAME" ]; then
+		dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
+			log_info "cannot delete device $DPLLSIM_CREATED_NAME"
+	fi
+	dpll_lib_cleanup
+}
+
+# Create a device named after the given clock-id and deploy it. Sets
+# DPLLSIM_CREATED_NAME.
+create_stress_device()
+{
+	local clock_id=$1
+	local name="st$clock_id"
+
+	dpllsim_create_device "$name" "$clock_id" || return 1
+
+	if ! dpllsim_deploy "$name"; then
+		dpllsim_delete_device "$name"
+		return 1
+	fi
+}
+
+##############################################################################
+
+test_rapid_create_delete()
+{
+	local i clock_id name
+
+	RET=0
+
+	for i in $(seq 1 "$STRESS_ITERATIONS"); do
+		dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
+
+		if ! create_stress_device "$clock_id"; then
+			check_err 1 "failed to create device (iteration $i)"
+			break
+		fi
+		name=$DPLLSIM_CREATED_NAME
+		track_device "$name"
+
+		if ! dpllsim_delete_device "$name"; then
+			check_err 1 "failed to delete device (iteration $i)"
+			break
+		fi
+		untrack_device "$name"
+	done
+
+	log_test "DPLL rapid create/delete ($STRESS_ITERATIONS iterations)"
+}
+
+test_deploy_cycle()
+{
+	local clock_id name i
+
+	RET=0
+
+	dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
+	name="cycle$clock_id"
+	if ! dpllsim_create_device "$name" "$clock_id"; then
+		check_err 1 "cannot create device"
+		log_test "DPLL deploy/undeploy cycling"
+		return
+	fi
+	track_device "$name"
+
+	for i in $(seq 1 "$STRESS_ITERATIONS"); do
+		if ! dpllsim_deploy "$name"; then
+			check_err 1 "deploy failed (iteration $i)"
+			break
+		fi
+		if ! dpll_device_wait_by_clock_id "$clock_id" dpllsim \
+		     > /dev/null; then
+			check_err 1 "device not on netlink (iteration $i)"
+			break
+		fi
+		if ! dpllsim_undeploy "$name"; then
+			check_err 1 "undeploy failed (iteration $i)"
+			break
+		fi
+		if ! dpll_device_wait_gone_by_clock_id "$clock_id" dpllsim; then
+			check_err 1 "device still on netlink (iteration $i)"
+			break
+		fi
+	done
+
+	if dpllsim_delete_device "$name"; then
+		untrack_device "$name"
+	else
+		check_err 1 "cannot delete device $name"
+	fi
+
+	log_test "DPLL deploy/undeploy cycling"
+}
+
+##############################################################################
+
+dpll_lib_check_prereqs
+# Another run would find the devices of this one left behind, see the
+# lock in the library.
+dpll_lib_lock
+
+# The module check may mount configfs, so the exit trap that unmounts it
+# again has to be in place before. INT and TERM exit through it as well,
+# so that a timeout imposed by the harness still removes the devices.
+trap cleanup EXIT
+trap 'exit 1' INT TERM
+dpllsim_check_module
+
+tests_run
+
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/drivers/net/dpll/settings b/tools/testing/selftests/drivers/net/dpll/settings
new file mode 100644
index 00000000000000..a62d2fa1275c6b
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/dpll/settings
@@ -0,0 +1 @@
+timeout=600
diff --git a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
new file mode 100644
index 00000000000000..fc81f149d1d0b7
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
@@ -0,0 +1,334 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Helpers for DPLL selftests. Provides wrappers around the iproute2 dpll
+# tool and around the dpllsim configfs interface for creating virtual DPLL
+# devices. Sourcing this file has no side effects; call
+# dpll_lib_check_prereqs and dpll_lib_init explicitly. net/lib.sh must be
+# sourced first, it provides ksft_skip, busywait and log_info.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+#
+# shellcheck disable=SC2034,SC2154 # the globals set here are read by the
+# scripts and ksft_skip is defined by net/lib.sh
+
+CONFIGFS_DIR="/sys/kernel/config"
+DPLLSIM_CFG="$CONFIGFS_DIR/dpllsim"
+
+# Next clock-id handed out by dpllsim_alloc_clock_id. The lookups of the
+# dpllsim helpers match on the module name as well, which is what keeps
+# the tests away from the devices of other drivers whatever clock-id
+# those use. The random start only avoids collisions between runs, as
+# the devices are named after their clock-id in configfs.
+DPLLSIM_NEXT_CLOCK_ID=$((96000 + RANDOM))
+
+##############################################################################
+# Prerequisites and device selection
+
+dpll_lib_check_prereqs()
+{
+	if [ "$EUID" -ne 0 ]; then
+		echo "SKIP: must be run as root"
+		exit "$ksft_skip"
+	fi
+
+	local cmd
+	for cmd in jq dpll; do
+		if ! command -v "$cmd" &> /dev/null; then
+			echo "SKIP: $cmd not found"
+			exit "$ksft_skip"
+		fi
+	done
+
+	if ! dpll device help 2>&1 | grep -q "show"; then
+		echo "SKIP: dpll tool too old, missing 'device show'"
+		exit "$ksft_skip"
+	fi
+}
+
+# The scripts that create dpllsim devices share the one dpllsim directory
+# and cannot run alongside each other, so each takes a lock on
+# DPLL_LOCK_FILE before it mounts or creates anything and skips when
+# another run holds it. The file lives under /run rather than in the
+# world-writable /tmp, where a planted symlink could redirect the open.
+DPLL_RUN_DIR="/run/dpll-selftest"
+DPLL_LOCK_FILE="$DPLL_RUN_DIR/lock"
+DPLL_LOCK_FD=""
+
+# Create DPLL_RUN_DIR when it is missing, skip when that fails.
+dpll_lib_run_dir()
+{
+	mkdir -p "$DPLL_RUN_DIR" 2>/dev/null
+	if ! [ -d "$DPLL_RUN_DIR" ]; then
+		echo "SKIP: cannot create $DPLL_RUN_DIR"
+		exit "$ksft_skip"
+	fi
+}
+
+dpll_lib_lock()
+{
+	if ! command -v flock &> /dev/null; then
+		echo "SKIP: flock not found"
+		exit "$ksft_skip"
+	fi
+	dpll_lib_run_dir
+	if ! { exec {DPLL_LOCK_FD}> "$DPLL_LOCK_FILE"; } 2>/dev/null; then
+		echo "SKIP: cannot open $DPLL_LOCK_FILE"
+		exit "$ksft_skip"
+	fi
+	if ! flock -n "$DPLL_LOCK_FD"; then
+		echo "SKIP: another dpll selftest run is in progress"
+		exit "$ksft_skip"
+	fi
+}
+
+# Detect the DPLL device to test. DPLL_DEV_ID may be set by the user to
+# force a specific device; an empty value means auto-detect, which takes
+# the first device that is not a dpllsim one, so that an instance of the
+# simulator left behind by an earlier run is not mistaken for hardware.
+# Sets DPLL_DEV_ID (may end up empty) and DPLL_MODULE_NAME.
+dpll_lib_init()
+{
+	DPLL_MODULE_NAME=""
+
+	if [ -n "${DPLL_DEV_ID:-}" ]; then
+		if ! dpll device show id "$DPLL_DEV_ID" &> /dev/null; then
+			echo "SKIP: DPLL device \"$DPLL_DEV_ID\" not found"
+			exit "$ksft_skip"
+		fi
+	else
+		DPLL_DEV_ID=$(dpll device show -j 2>/dev/null | \
+			      jq -r '[.device[]? |
+				      select(.["module-name"] != "dpllsim")
+				     ][0].id // empty')
+	fi
+
+	if [ -n "$DPLL_DEV_ID" ]; then
+		DPLL_MODULE_NAME=$(dpll_device_get_module "$DPLL_DEV_ID")
+	fi
+}
+
+##############################################################################
+# Device queries
+#
+# "dpll device show id X -j" may print either a flat object or a dump-style
+# {"device":[...]} wrapper depending on the iproute2 version, so normalize
+# with (.device[0]? // .) before extracting fields. The tool is run first
+# and jq only on what it printed, so that a failed query returns its
+# status instead of the empty output a missing field produces too.
+
+dpll_device_json()
+{
+	local dev_id=$1
+	local out
+
+	out=$(dpll device show id "$dev_id" -j 2>/dev/null) || return 1
+	jq '(.device[0]? // .)' <<< "$out"
+}
+
+__dpll_device_field()
+{
+	local dev_id=$1; shift
+	local out
+
+	out=$(dpll device show id "$dev_id" -j 2>/dev/null) || return 1
+	jq -r "(.device[0]? // .) | $1 // empty" <<< "$out"
+}
+
+dpll_device_get_module()
+{
+	__dpll_device_field "$1" '.["module-name"]'
+}
+
+# Look up the netlink device id by the clock-id chosen at create time,
+# restricted to the devices of the given module when one is named. The
+# clock-id is the only reliable handle on a device the test created, as
+# netlink ids depend on what else exists, and the module name keeps a
+# lookup for a dpllsim device from returning a device of another driver
+# that happens to use the same clock-id. The clock-id is compared as a
+# string, so that it does not matter whether the tool prints it as a
+# JSON number or as a string. Return 1 when the dump itself failed, a
+# caller must not take that for a device that is gone.
+# $1: clock-id, $2: module name (optional)
+dpll_device_get_id_by_clock_id()
+{
+	local clock_id=$1
+	local module=${2:-}
+	local out
+
+	out=$(dpll device show -j 2>/dev/null) || return 1
+	jq -r --arg cid "$clock_id" --arg mod "$module" \
+	   '[.device[]? |
+	     select((.["clock-id"] | tostring) == $cid and
+		    ($mod == "" or .["module-name"] == $mod))
+	    ][0].id // empty' <<< "$out"
+}
+
+# The two checks below fail when the lookup itself failed, so that a
+# failed dump neither counts as a device that is present nor as one
+# that is gone, the wait helpers keep polling instead.
+__dpll_device_present_by_clock_id()
+{
+	local dev_id
+
+	dev_id=$(dpll_device_get_id_by_clock_id "$@") || return 1
+	[ -n "$dev_id" ] && echo "$dev_id"
+}
+
+__dpll_device_absent_by_clock_id()
+{
+	local dev_id
+
+	dev_id=$(dpll_device_get_id_by_clock_id "$@") || return 1
+	[ -z "$dev_id" ]
+}
+
+# Wait until a device with the given clock-id (and module, when given) is
+# visible on netlink and print its netlink id. Callers must check the
+# exit status rather than the output, busywait prints the output of the
+# last failed check when it times out.
+dpll_device_wait_by_clock_id()
+{
+	busywait "$BUSYWAIT_TIMEOUT" __dpll_device_present_by_clock_id "$@"
+}
+
+dpll_device_wait_gone_by_clock_id()
+{
+	busywait "$BUSYWAIT_TIMEOUT" __dpll_device_absent_by_clock_id "$@"
+}
+
+##############################################################################
+# dpllsim configfs interface
+#
+# Each directory under $DPLLSIM_CFG is one simulated device. Writing 1 to
+# <dev>/live registers the device with the DPLL core, writing 0
+# unregisters it. The structure (clock_id) can only be changed while the
+# device is not live.
+#
+# dpllsim_available mounts configfs when nothing else did, which is the
+# one side effect the library has outside the dpllsim directory. Scripts
+# call dpll_lib_cleanup last from their exit trap to undo it.
+
+DPLLSIM_MOUNTED_CONFIGFS=0
+
+dpllsim_available()
+{
+	[ -d "$DPLLSIM_CFG" ] && return 0
+
+	modprobe dpllsim &> /dev/null
+	if ! mountpoint -q "$CONFIGFS_DIR"; then
+		mount -t configfs none "$CONFIGFS_DIR" &> /dev/null &&
+			DPLLSIM_MOUNTED_CONFIGFS=1
+	fi
+	[ -d "$DPLLSIM_CFG" ]
+}
+
+# Unmount configfs again when dpllsim_available mounted it. Call after
+# the devices of the script are gone. A dpllsim device the script could
+# not remove keeps the mount, so that what is left can be looked at.
+dpll_lib_cleanup()
+{
+	local dev_path name
+
+	[ "$DPLLSIM_MOUNTED_CONFIGFS" -eq 1 ] || return 0
+	DPLLSIM_MOUNTED_CONFIGFS=0
+
+	for dev_path in "$DPLLSIM_CFG"/*/; do
+		[ -d "$dev_path" ] || continue
+		name=$(basename "$dev_path")
+		log_info "device $name left behind, $CONFIGFS_DIR kept mounted"
+		return
+	done
+	umount "$CONFIGFS_DIR" 2>/dev/null ||
+		log_info "cannot unmount $CONFIGFS_DIR"
+}
+
+dpllsim_check_module()
+{
+	if ! dpllsim_available; then
+		echo "SKIP: dpllsim module not available"
+		exit "$ksft_skip"
+	fi
+}
+
+# $1: device name, $2: clock-id. Sets DPLLSIM_CREATED_NAME as soon as the
+# directory exists, so that the cleanup handler of a script can remove a
+# device whose creation a signal interrupted before the script recorded
+# it. The name stays set once the device is complete.
+dpllsim_create_device()
+{
+	local name=$1
+	local clock_id=$2
+	local dev_path="$DPLLSIM_CFG/$name"
+
+	# Record the name before the directory exists, so that a cleanup
+	# run by a signal between the two still finds it.
+	DPLLSIM_CREATED_NAME=$name
+	if ! mkdir "$dev_path" 2>/dev/null; then
+		DPLLSIM_CREATED_NAME=""
+		return 1
+	fi
+	if ! echo "$clock_id" 2>/dev/null > "$dev_path/clock_id"; then
+		rmdir "$dev_path" 2>/dev/null
+		DPLLSIM_CREATED_NAME=""
+		return 1
+	fi
+}
+
+dpllsim_deploy()
+{
+	echo 1 2>/dev/null > "$DPLLSIM_CFG/$1/live"
+}
+
+dpllsim_undeploy()
+{
+	echo 0 2>/dev/null > "$DPLLSIM_CFG/$1/live"
+}
+
+# Take the device offline and remove it. Every step is attempted even
+# when an earlier one failed, the return value is non-zero when any did.
+# A device that does not exist counts as deleted.
+dpllsim_delete_device()
+{
+	local dev_path="$DPLLSIM_CFG/$1"
+	local ret=0
+
+	[ -d "$dev_path" ] || return 0
+
+	dpllsim_undeploy "$1" || ret=1
+	rmdir "$dev_path" || ret=1
+
+	return "$ret"
+}
+
+# Set DPLLSIM_CLOCK_ID to a clock-id unique within this test run. Must
+# not be called from a subshell (the counter increment would be lost);
+# derive per-worker id ranges in the parent instead.
+dpllsim_alloc_clock_id()
+{
+	DPLLSIM_CLOCK_ID=$((DPLLSIM_NEXT_CLOCK_ID++))
+}
+
+# Create a device, deploy it and resolve the netlink device id. Sets
+# DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and DPLLSIM_DEV_ID.
+# shellcheck disable=SC2120 # the argument is optional
+dpllsim_setup_standard_device()
+{
+	local clock_id name
+
+	dpllsim_alloc_clock_id
+	clock_id=$DPLLSIM_CLOCK_ID
+	name="std$clock_id"
+	dpllsim_create_device "$name" "$clock_id" || return 1
+
+	if ! dpllsim_deploy "$name"; then
+		dpllsim_delete_device "$name"
+		return 1
+	fi
+
+	if ! DPLLSIM_DEV_ID=$(dpll_device_wait_by_clock_id "$clock_id" \
+							  dpllsim); then
+		dpllsim_delete_device "$name"
+		return 1
+	fi
+}
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 03/14] dpll: dpllsim: Add configurable pins
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
  2026-11-05 15:18 ` [PATCH net-next v2 01/14] dpll: dpllsim: Add " Petr Oros
  2026-11-05 15:18 ` [PATCH net-next v2 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:48   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 04/14] selftests: dpll: add pin enumeration and property tests Petr Oros
                   ` (11 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Pins are the unit of most DPLL netlink interaction, so let a device
instance carry an arbitrary set of them. Each directory created under
the pins/ group of an instance is one pin with type, frequency and
direction attributes. The type takes the names of the DPLL pin type
enum as iproute2 prints them, the direction is input or output and the
frequency must be one of the frequencies the driver advertises as
supported for the type. The pin directory name becomes the board label
of the pin, which gives tests a stable handle to find a pin by. The
attributes are writable only while the instance is not live and return
EBUSY otherwise.

At live=1 the pins are ordered as created and each pin records its
index in that order, which is the pin index it is registered under.
Their frequencies are validated against the per type tables, a MUX pin
configured as output is rejected since a MUX pin stands for an input
selector, and all pin handles are obtained up front, so that a failure
leaves nothing half visible. The device is registered first and the
pins after it, so that every pin registration event reaches userspace
with a valid device, and a failure unwinds exactly what was registered
in reverse order. While the instance is live the pin items are held
with configfs_depend_item_unlocked(), which makes rmdir of a pin fail
with EBUSY until the instance is taken down.

The pins register with the mandatory callbacks only, direction_get and
state_on_dpll_get, plus frequency_get since the frequency is known from
the configuration. The input and output ops tables are identical here
and diverge when input pins gain their setters in the next patches. The
state is always reported as disconnected for now, the settable pin
features follow in separate patches.

Signed-off-by: Petr Oros <poros@redhat.com>
---
 Documentation/driver-api/dpllsim.rst |  40 +++-
 drivers/dpll/dpllsim/core.c          | 225 ++++++++++++++++++++-
 drivers/dpll/dpllsim/dpll.c          | 280 ++++++++++++++++++++++++++-
 drivers/dpll/dpllsim/dpllsim.h       |  32 +++
 4 files changed, 565 insertions(+), 12 deletions(-)

diff --git a/Documentation/driver-api/dpllsim.rst b/Documentation/driver-api/dpllsim.rst
index 86a5d523534055..e7c5ee93392c21 100644
--- a/Documentation/driver-api/dpllsim.rst
+++ b/Documentation/driver-api/dpllsim.rst
@@ -12,10 +12,12 @@ Configuration interface
 =======================
 
 Devices are described in configfs under ``/sys/kernel/config/dpllsim/``.
-A directory created there is a device instance. Attributes are written
-while the instance is not live, then ``live`` is set to 1 to register
-the device with the DPLL subsystem. Writing 0 unregisters it again.
-Structural attributes return ``EBUSY`` while the instance is live.
+A directory created there is a device instance, a directory created in
+its ``pins/`` subdirectory is a pin. Attributes are written while the
+instance is not live, then ``live`` is set to 1 to register the device
+and its pins with the DPLL subsystem. Writing 0 unregisters everything
+again. Structural attributes return ``EBUSY`` while the instance is live
+and pins of a live instance cannot be removed.
 
 Device directory ``/sys/kernel/config/dpllsim/<dev>/``:
 
@@ -25,17 +27,41 @@ Device directory ``/sys/kernel/config/dpllsim/<dev>/``:
 ``live``                 1 registers the device, 0 unregisters it
 =======================  =====================================================
 
+Pin directory ``/sys/kernel/config/dpllsim/<dev>/pins/<pin>/``:
+
+=============  ===============================================================
+``type``       ``gnss``, ``int-oscillator``, ``ext``, ``synce-eth-port`` or
+               ``mux`` (default ``ext``)
+``frequency``  Frequency in Hz, must be supported by the pin type
+               (default 0, which is rejected)
+``direction``  ``input`` or ``output`` (default ``input``), a ``mux`` pin
+               must be an input
+=============  ===============================================================
+
+The pin directory name becomes the board label of the pin. Pins are
+registered in the order the directories were created.
+
 Example
 =======
 
-Create a device and make it live::
+Create a device with a GNSS input, an external input and an external
+output, then make it live::
 
     cd /sys/kernel/config/dpllsim
     mkdir dev0 && cd dev0
     echo 100 > clock_id
+    mkdir pins/gnss0
+    echo gnss > pins/gnss0/type
+    echo 1 > pins/gnss0/frequency
+    mkdir pins/ext0
+    echo 10000000 > pins/ext0/frequency
+    mkdir pins/out0
+    echo 10000000 > pins/out0/frequency
+    echo output > pins/out0/direction
     echo 1 > live
 
     dpll device show
+    dpll pin show
 
-To remove the device, write 0 to ``live`` and remove the device
-directory.
+To remove the device, write 0 to ``live``, remove the pin directories
+and finally the device directory.
diff --git a/drivers/dpll/dpllsim/core.c b/drivers/dpll/dpllsim/core.c
index 0772f409d013b7..97b3cf48772e7f 100644
--- a/drivers/dpll/dpllsim/core.c
+++ b/drivers/dpll/dpllsim/core.c
@@ -6,12 +6,14 @@
 
 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 
+#include <linux/array_size.h>
 #include <linux/cleanup.h>
 #include <linux/configfs.h>
 #include <linux/kstrtox.h>
 #include <linux/module.h>
 #include <linux/mutex.h>
 #include <linux/slab.h>
+#include <linux/string.h>
 #include <linux/sysfs.h>
 
 #include "dpllsim.h"
@@ -23,6 +25,182 @@ static struct dpllsim_dev *to_dpllsim_dev(struct config_item *item)
 	return container_of(to_config_group(item), struct dpllsim_dev, group);
 }
 
+static struct dpllsim_pin *to_dpllsim_pin(struct config_item *item)
+{
+	return container_of(item, struct dpllsim_pin, item);
+}
+
+static const struct {
+	const char *name;
+	enum dpll_pin_type type;
+} dpllsim_pin_types[] = {
+	{ "gnss", DPLL_PIN_TYPE_GNSS },
+	{ "int-oscillator", DPLL_PIN_TYPE_INT_OSCILLATOR },
+	{ "ext", DPLL_PIN_TYPE_EXT },
+	{ "synce-eth-port", DPLL_PIN_TYPE_SYNCE_ETH_PORT },
+	{ "mux", DPLL_PIN_TYPE_MUX },
+};
+
+static ssize_t dpllsim_pin_type_show(struct config_item *item, char *page)
+{
+	struct dpllsim_pin *pin = to_dpllsim_pin(item);
+	int i;
+
+	guard(mutex)(&pin->dev->lock);
+	for (i = 0; i < ARRAY_SIZE(dpllsim_pin_types); i++)
+		if (dpllsim_pin_types[i].type == pin->type)
+			return sysfs_emit(page, "%s\n",
+					  dpllsim_pin_types[i].name);
+	return -EINVAL;
+}
+
+static ssize_t dpllsim_pin_type_store(struct config_item *item,
+				      const char *page, size_t count)
+{
+	struct dpllsim_pin *pin = to_dpllsim_pin(item);
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(dpllsim_pin_types); i++)
+		if (sysfs_streq(page, dpllsim_pin_types[i].name))
+			break;
+	if (i == ARRAY_SIZE(dpllsim_pin_types))
+		return -EINVAL;
+
+	guard(mutex)(&pin->dev->lock);
+	if (dpllsim_dev_frozen(pin->dev))
+		return -EBUSY;
+	pin->type = dpllsim_pin_types[i].type;
+	return count;
+}
+CONFIGFS_ATTR(dpllsim_pin_, type);
+
+static ssize_t dpllsim_pin_direction_show(struct config_item *item, char *page)
+{
+	struct dpllsim_pin *pin = to_dpllsim_pin(item);
+
+	guard(mutex)(&pin->dev->lock);
+	return sysfs_emit(page, "%s\n",
+			  pin->direction == DPLL_PIN_DIRECTION_INPUT ?
+			  "input" : "output");
+}
+
+static ssize_t dpllsim_pin_direction_store(struct config_item *item,
+					   const char *page, size_t count)
+{
+	struct dpllsim_pin *pin = to_dpllsim_pin(item);
+	enum dpll_pin_direction direction;
+
+	if (sysfs_streq(page, "input"))
+		direction = DPLL_PIN_DIRECTION_INPUT;
+	else if (sysfs_streq(page, "output"))
+		direction = DPLL_PIN_DIRECTION_OUTPUT;
+	else
+		return -EINVAL;
+
+	guard(mutex)(&pin->dev->lock);
+	if (dpllsim_dev_frozen(pin->dev))
+		return -EBUSY;
+	pin->direction = direction;
+	return count;
+}
+CONFIGFS_ATTR(dpllsim_pin_, direction);
+
+static ssize_t dpllsim_pin_frequency_show(struct config_item *item, char *page)
+{
+	struct dpllsim_pin *pin = to_dpllsim_pin(item);
+
+	guard(mutex)(&pin->dev->lock);
+	return sysfs_emit(page, "%llu\n", pin->frequency);
+}
+
+static ssize_t dpllsim_pin_frequency_store(struct config_item *item,
+					   const char *page, size_t count)
+{
+	struct dpllsim_pin *pin = to_dpllsim_pin(item);
+	u64 val;
+	int err;
+
+	err = kstrtou64(page, 10, &val);
+	if (err)
+		return err;
+
+	guard(mutex)(&pin->dev->lock);
+	if (dpllsim_dev_frozen(pin->dev))
+		return -EBUSY;
+	pin->frequency = val;
+	return count;
+}
+CONFIGFS_ATTR(dpllsim_pin_, frequency);
+
+static struct configfs_attribute *dpllsim_pin_attrs[] = {
+	&dpllsim_pin_attr_type,
+	&dpllsim_pin_attr_direction,
+	&dpllsim_pin_attr_frequency,
+	NULL,
+};
+
+static void dpllsim_pin_release(struct config_item *item)
+{
+	struct dpllsim_pin *pin = to_dpllsim_pin(item);
+
+	kfree(pin);
+}
+
+static const struct configfs_item_operations dpllsim_pin_item_ops = {
+	.release	= dpllsim_pin_release,
+};
+
+static const struct config_item_type dpllsim_pin_type = {
+	.ct_item_ops	= &dpllsim_pin_item_ops,
+	.ct_attrs	= dpllsim_pin_attrs,
+	.ct_owner	= THIS_MODULE,
+};
+
+static struct config_item *dpllsim_pins_make_item(struct config_group *group,
+						  const char *name)
+{
+	struct dpllsim_dev *sim = container_of(group, struct dpllsim_dev,
+					       pins_group);
+	struct dpllsim_pin *pin;
+
+	lockdep_assert_held(&dpllsim_subsys.su_mutex);
+
+	if (dpllsim_dev_frozen(sim))
+		return ERR_PTR(-EBUSY);
+	if (list_count_nodes(&sim->pin_list) >= DPLLSIM_MAX_PINS)
+		return ERR_PTR(-ENOSPC);
+
+	pin = kzalloc_obj(*pin);
+	if (!pin)
+		return ERR_PTR(-ENOMEM);
+
+	config_item_init_type_name(&pin->item, name, &dpllsim_pin_type);
+	pin->dev = sim;
+	pin->type = DPLL_PIN_TYPE_EXT;
+	pin->direction = DPLL_PIN_DIRECTION_INPUT;
+	list_add_tail(&pin->list, &sim->pin_list);
+
+	return &pin->item;
+}
+
+/* Pin mkdir and rmdir run under the subsystem mutex like live transitions */
+static void dpllsim_pins_drop_item(struct config_group *group,
+				   struct config_item *item)
+{
+	list_del(&to_dpllsim_pin(item)->list);
+	config_item_put(item);
+}
+
+static const struct configfs_group_operations dpllsim_pins_group_ops = {
+	.make_item	= dpllsim_pins_make_item,
+	.drop_item	= dpllsim_pins_drop_item,
+};
+
+static const struct config_item_type dpllsim_pins_type = {
+	.ct_group_ops	= &dpllsim_pins_group_ops,
+	.ct_owner	= THIS_MODULE,
+};
+
 /* The clock id is checked at live=1, so both share the subsystem mutex */
 static ssize_t dpllsim_dev_clock_id_show(struct config_item *item, char *page)
 {
@@ -82,9 +260,39 @@ static bool dpllsim_clock_id_in_use(struct dpllsim_dev *sim)
 	return false;
 }
 
+/* Pins of a live device cannot be removed */
+static void dpllsim_dev_undepend_pins(struct dpllsim_dev *sim,
+				      struct dpllsim_pin *last)
+{
+	struct dpllsim_pin *pin;
+
+	list_for_each_entry(pin, &sim->pin_list, list) {
+		if (pin == last)
+			break;
+		configfs_undepend_item_unlocked(&pin->item);
+	}
+}
+
+static int dpllsim_dev_depend_pins(struct dpllsim_dev *sim)
+{
+	struct dpllsim_pin *pin;
+	int err;
+
+	list_for_each_entry(pin, &sim->pin_list, list) {
+		err = configfs_depend_item_unlocked(&dpllsim_subsys,
+						    &pin->item);
+		if (err) {
+			dpllsim_dev_undepend_pins(sim, pin);
+			return err;
+		}
+	}
+
+	return 0;
+}
+
 /*
- * The subsystem mutex excludes the live transitions of the other
- * instances for the whole deploy sequence.
+ * The subsystem mutex excludes pin mkdir and rmdir and the live
+ * transitions of the other instances for the whole deploy sequence.
  */
 static ssize_t dpllsim_dev_live_store(struct config_item *item,
 				      const char *page, size_t count)
@@ -104,6 +312,7 @@ static ssize_t dpllsim_dev_live_store(struct config_item *item,
 
 	if (!live) {
 		dpllsim_dev_undeploy(sim);
+		dpllsim_dev_undepend_pins(sim, NULL);
 		return count;
 	}
 
@@ -112,10 +321,16 @@ static ssize_t dpllsim_dev_live_store(struct config_item *item,
 	if (dpllsim_clock_id_in_use(sim))
 		return -EEXIST;
 
-	err = dpllsim_dev_deploy(sim);
+	err = dpllsim_dev_depend_pins(sim);
 	if (err)
 		return err;
 
+	err = dpllsim_dev_deploy(sim);
+	if (err) {
+		dpllsim_dev_undepend_pins(sim, NULL);
+		return err;
+	}
+
 	return count;
 }
 CONFIGFS_ATTR(dpllsim_dev_, live);
@@ -154,11 +369,15 @@ static struct config_group *dpllsim_make_group(struct config_group *group,
 		return ERR_PTR(-ENOMEM);
 
 	mutex_init(&sim->lock);
+	INIT_LIST_HEAD(&sim->pin_list);
 	sim->mode = DPLL_MODE_AUTOMATIC;
 	sim->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
 	sim->lock_status_error = DPLL_LOCK_STATUS_ERROR_NONE;
 
 	config_group_init_type_name(&sim->group, name, &dpllsim_dev_type);
+	config_group_init_type_name(&sim->pins_group, "pins",
+				    &dpllsim_pins_type);
+	configfs_add_default_group(&sim->pins_group, &sim->group);
 
 	return &sim->group;
 }
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index ae2c8fcf238c68..a1d5921fb8d017 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -4,12 +4,39 @@
  * Copyright (c) 2026 Red Hat, Inc.
  */
 
+#include <linux/array_size.h>
 #include <linux/cleanup.h>
 #include <linux/dpll.h>
 #include <linux/module.h>
+#include <linux/slab.h>
 
 #include "dpllsim.h"
 
+static struct dpll_pin_frequency dpllsim_freq_1pps[] = {
+	DPLL_PIN_FREQUENCY_RANGE(1, 1),
+};
+
+static struct dpll_pin_frequency dpllsim_freq_synce[] = {
+	DPLL_PIN_FREQUENCY_RANGE(1544000, 1544000),
+	DPLL_PIN_FREQUENCY_RANGE(2048000, 2048000),
+	DPLL_PIN_FREQUENCY_RANGE(10000000, 10000000),
+	DPLL_PIN_FREQUENCY_RANGE(25000000, 25000000),
+};
+
+static struct dpll_pin_frequency dpllsim_freq_ext[] = {
+	DPLL_PIN_FREQUENCY_RANGE(1, 1),
+	DPLL_PIN_FREQUENCY_RANGE(10000000, 10000000),
+	DPLL_PIN_FREQUENCY_RANGE(19440000, 19440000),
+	DPLL_PIN_FREQUENCY_RANGE(25000000, 25000000),
+	DPLL_PIN_FREQUENCY_RANGE(77760000, 77760000),
+	DPLL_PIN_FREQUENCY_RANGE(125000000, 125000000),
+	DPLL_PIN_FREQUENCY_RANGE(156250000, 156250000),
+};
+
+static struct dpll_pin_frequency dpllsim_freq_int_osc[] = {
+	DPLL_PIN_FREQUENCY_RANGE(25000000, 25000000),
+};
+
 static int dpllsim_mode_get(const struct dpll_device *dpll, void *priv,
 			    enum dpll_mode *mode,
 			    struct netlink_ext_ack *extack)
@@ -39,6 +66,229 @@ static const struct dpll_device_ops dpllsim_dpll_ops = {
 	.lock_status_get = dpllsim_lock_status_get,
 };
 
+static int dpllsim_pin_frequency_get(const struct dpll_pin *pin,
+				     void *pin_priv,
+				     const struct dpll_device *dpll,
+				     void *dpll_priv, u64 *frequency,
+				     struct netlink_ext_ack *extack)
+{
+	struct dpllsim_pin *sim_pin = pin_priv;
+
+	guard(mutex)(&sim_pin->dev->lock);
+	*frequency = sim_pin->frequency;
+	return 0;
+}
+
+static int dpllsim_pin_direction_get(const struct dpll_pin *pin,
+				     void *pin_priv,
+				     const struct dpll_device *dpll,
+				     void *dpll_priv,
+				     enum dpll_pin_direction *direction,
+				     struct netlink_ext_ack *extack)
+{
+	struct dpllsim_pin *sim_pin = pin_priv;
+
+	*direction = sim_pin->direction;
+	return 0;
+}
+
+static int dpllsim_pin_state_get(const struct dpll_pin *pin, void *pin_priv,
+				 const struct dpll_device *dpll,
+				 void *dpll_priv, enum dpll_pin_state *state,
+				 struct netlink_ext_ack *extack)
+{
+	struct dpllsim_pin *sim_pin = pin_priv;
+
+	guard(mutex)(&sim_pin->dev->lock);
+	*state = sim_pin->state;
+	return 0;
+}
+
+static const struct dpll_pin_ops dpllsim_input_pin_ops = {
+	.frequency_get = dpllsim_pin_frequency_get,
+	.direction_get = dpllsim_pin_direction_get,
+	.state_on_dpll_get = dpllsim_pin_state_get,
+};
+
+static const struct dpll_pin_ops dpllsim_output_pin_ops = {
+	.frequency_get = dpllsim_pin_frequency_get,
+	.direction_get = dpllsim_pin_direction_get,
+	.state_on_dpll_get = dpllsim_pin_state_get,
+};
+
+static const struct dpll_pin_ops *
+dpllsim_pin_select_ops(const struct dpllsim_pin *pin)
+{
+	if (pin->direction == DPLL_PIN_DIRECTION_INPUT)
+		return &dpllsim_input_pin_ops;
+	return &dpllsim_output_pin_ops;
+}
+
+static void dpllsim_pin_props(struct dpllsim_pin *pin,
+			      struct dpll_pin_properties *prop)
+{
+	prop->type = pin->type;
+	prop->board_label = config_item_name(&pin->item);
+
+	switch (pin->type) {
+	case DPLL_PIN_TYPE_GNSS:
+		prop->freq_supported = dpllsim_freq_1pps;
+		prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_1pps);
+		break;
+	case DPLL_PIN_TYPE_INT_OSCILLATOR:
+		prop->freq_supported = dpllsim_freq_int_osc;
+		prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_int_osc);
+		break;
+	case DPLL_PIN_TYPE_MUX:
+	case DPLL_PIN_TYPE_SYNCE_ETH_PORT:
+		prop->freq_supported = dpllsim_freq_synce;
+		prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_synce);
+		break;
+	default:
+		prop->freq_supported = dpllsim_freq_ext;
+		prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_ext);
+		break;
+	}
+}
+
+static bool dpllsim_pin_frequency_valid(struct dpllsim_pin *pin)
+{
+	struct dpll_pin_properties prop = {};
+	int i;
+
+	dpllsim_pin_props(pin, &prop);
+	for (i = 0; i < prop.freq_supported_num; i++)
+		if (pin->frequency >= prop.freq_supported[i].min &&
+		    pin->frequency <= prop.freq_supported[i].max)
+			return true;
+
+	return false;
+}
+
+/* A MUX pin stands for an input selector, so it is an input itself */
+static bool dpllsim_pin_direction_valid(const struct dpllsim_pin *pin)
+{
+	return pin->type != DPLL_PIN_TYPE_MUX ||
+	       pin->direction == DPLL_PIN_DIRECTION_INPUT;
+}
+
+static void dpllsim_pin_reset(struct dpllsim_pin *pin)
+{
+	pin->ops = dpllsim_pin_select_ops(pin);
+	pin->state = DPLL_PIN_STATE_DISCONNECTED;
+}
+
+/* Build sim->pins in registration order, the order the pins were created */
+static int dpllsim_order_pins(struct dpllsim_dev *sim)
+{
+	struct dpllsim_pin *pin;
+	int count = 0, i;
+
+	list_for_each_entry(pin, &sim->pin_list, list) {
+		if (!dpllsim_pin_frequency_valid(pin) ||
+		    !dpllsim_pin_direction_valid(pin))
+			return -EINVAL;
+		count++;
+	}
+
+	sim->pins = kzalloc_objs(*sim->pins, count);
+	if (!sim->pins)
+		return -ENOMEM;
+
+	sim->num_pins = 0;
+	list_for_each_entry(pin, &sim->pin_list, list) {
+		pin->idx = sim->num_pins;
+		sim->pins[sim->num_pins++] = pin;
+	}
+
+	for (i = 0; i < sim->num_pins; i++)
+		dpllsim_pin_reset(sim->pins[i]);
+
+	return 0;
+}
+
+static void dpllsim_put_pins(struct dpllsim_dev *sim)
+{
+	int i;
+
+	for (i = sim->num_pins - 1; i >= 0; i--) {
+		struct dpllsim_pin *pin = sim->pins[i];
+
+		if (pin->dpll_pin) {
+			dpll_pin_put(pin->dpll_pin, &pin->tracker);
+			pin->dpll_pin = NULL;
+		}
+	}
+}
+
+static int dpllsim_get_pins(struct dpllsim_dev *sim)
+{
+	int i, err;
+
+	for (i = 0; i < sim->num_pins; i++) {
+		struct dpllsim_pin *pin = sim->pins[i];
+		struct dpll_pin_properties prop = {};
+		struct dpll_pin *dpll_pin;
+
+		dpllsim_pin_props(pin, &prop);
+		dpll_pin = dpll_pin_get(sim->clock_id, pin->idx, THIS_MODULE,
+					&prop, &pin->tracker);
+		if (IS_ERR(dpll_pin)) {
+			err = PTR_ERR(dpll_pin);
+			goto err_put_pins;
+		}
+		pin->dpll_pin = dpll_pin;
+	}
+
+	return 0;
+
+err_put_pins:
+	dpllsim_put_pins(sim);
+	return err;
+}
+
+static void dpllsim_unregister_pins(struct dpllsim_dev *sim)
+{
+	int i;
+
+	for (i = sim->num_pins - 1; i >= 0; i--) {
+		struct dpllsim_pin *pin = sim->pins[i];
+
+		if (pin->registered)
+			dpll_pin_unregister(sim->dpll, pin->dpll_pin, pin->ops,
+					    pin);
+		pin->registered = false;
+	}
+}
+
+static int dpllsim_register_pins(struct dpllsim_dev *sim)
+{
+	struct dpllsim_pin *pin;
+	int i, err;
+
+	for (i = 0; i < sim->num_pins; i++) {
+		pin = sim->pins[i];
+
+		err = dpll_pin_register(sim->dpll, pin->dpll_pin, pin->ops,
+					pin);
+		if (err)
+			goto err_unregister;
+		pin->registered = true;
+	}
+
+	return 0;
+
+err_unregister:
+	dpllsim_unregister_pins(sim);
+	return err;
+}
+
+/*
+ * Registration order matters: the device must be registered before pins
+ * so that pin registration events reach userspace with a valid device,
+ * while pin handles must be obtained up front so that a failure leaves
+ * nothing half-visible.
+ */
 int dpllsim_dev_deploy(struct dpllsim_dev *sim)
 {
 	struct dpll_device *dpll;
@@ -48,15 +298,27 @@ int dpllsim_dev_deploy(struct dpllsim_dev *sim)
 	if (IS_ERR(dpll))
 		return PTR_ERR(dpll);
 
-	/* From here on the configuration is frozen */
+	/* From here on the pin configuration is frozen */
 	mutex_lock(&sim->lock);
 	sim->dpll = dpll;
 	mutex_unlock(&sim->lock);
 
+	err = dpllsim_order_pins(sim);
+	if (err)
+		goto err_put_dpll;
+
+	err = dpllsim_get_pins(sim);
+	if (err)
+		goto err_free_pins;
+
 	err = dpll_device_register(sim->dpll, DPLL_TYPE_PPS,
 				   &dpllsim_dpll_ops, sim);
 	if (err)
-		goto err_put_dpll;
+		goto err_put_pins;
+
+	err = dpllsim_register_pins(sim);
+	if (err)
+		goto err_unregister_device;
 
 	mutex_lock(&sim->lock);
 	sim->registered = true;
@@ -64,6 +326,14 @@ int dpllsim_dev_deploy(struct dpllsim_dev *sim)
 
 	return 0;
 
+err_unregister_device:
+	dpll_device_unregister(sim->dpll, &dpllsim_dpll_ops, sim);
+err_put_pins:
+	dpllsim_put_pins(sim);
+err_free_pins:
+	kfree(sim->pins);
+	sim->pins = NULL;
+	sim->num_pins = 0;
 err_put_dpll:
 	dpll_device_put(sim->dpll, &sim->tracker);
 	mutex_lock(&sim->lock);
@@ -77,8 +347,14 @@ void dpllsim_dev_undeploy(struct dpllsim_dev *sim)
 	scoped_guard(mutex, &sim->lock)
 		sim->registered = false;
 
+	dpllsim_unregister_pins(sim);
 	dpll_device_unregister(sim->dpll, &dpllsim_dpll_ops, sim);
+	dpllsim_put_pins(sim);
 	dpll_device_put(sim->dpll, &sim->tracker);
 	scoped_guard(mutex, &sim->lock)
 		sim->dpll = NULL;
+
+	kfree(sim->pins);
+	sim->pins = NULL;
+	sim->num_pins = 0;
 }
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
index 5667f8827c98c1..73b846b66f79c7 100644
--- a/drivers/dpll/dpllsim/dpllsim.h
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -9,10 +9,39 @@
 
 #include <linux/configfs.h>
 #include <linux/dpll.h>
+#include <linux/list.h>
 #include <linux/mutex.h>
 
+#define DPLLSIM_MAX_PINS			128
+
+struct dpllsim_dev;
+
+struct dpllsim_pin {
+	struct config_item item;
+	struct list_head list;
+	struct dpllsim_dev *dev;
+
+	/* Configuration, writable only until dpllsim_dev_frozen() */
+	enum dpll_pin_type type;
+	enum dpll_pin_direction direction;
+	u64 frequency;
+
+	/* Resolved at live=1 */
+	int idx;
+	struct dpll_pin *dpll_pin;
+	const struct dpll_pin_ops *ops;
+	dpll_tracker tracker;
+	bool registered;
+
+	/* Simulated state, guarded by dev->lock */
+	enum dpll_pin_state state;
+};
+
 struct dpllsim_dev {
 	struct config_group group;
+	struct config_group pins_group;
+	/* Pin items, guarded by the configfs subsystem mutex */
+	struct list_head pin_list;
 
 	/*
 	 * Set from the start of a live transition until the device is
@@ -27,6 +56,9 @@ struct dpllsim_dev {
 	 * the live attribute reports, see dpllsim_dev_live().
 	 */
 	bool registered;
+	/* Pins in registration order, valid while @dpll is set */
+	struct dpllsim_pin **pins;
+	int num_pins;
 
 	/* Guarded by the subsystem mutex */
 	u64 clock_id;
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 04/14] selftests: dpll: add pin enumeration and property tests
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (2 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 03/14] dpll: dpllsim: Add configurable pins Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:48   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 05/14] dpll: dpllsim: Add support to set input pin state Petr Oros
                   ` (10 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Extend the library with pin queries. Pins of a device are collected
from the full pin dump with jq, since older iproute2 lacks the device
filter of "pin show", and the fields that live in the parent-device
nest are read through a helper that selects the nest of the device
under test. The helpers that work on the full dump normalize its shape
the same way the id-based ones do, so that a tool printing a flat
object instead of the dump wrapper works with all of them. Like the
device queries, every pin query runs the tool first and jq on what it
printed, so that a query which fails returns the status of the tool
instead of the empty output a missing attribute produces too. The
prerequisite check now also requires a tool that knows "pin show", so
that a tool too old for the pin queries is a skip up front rather than
a failed query in every test. The dpllsim helpers gain
dpllsim_add_pin, which creates a pin directory and writes its type,
frequency and direction, and the standard device grows a GNSS input,
an external input and an external output.

dpll_basic.sh checks that the enumeration yields a numeric id for every
pin of the device, the ids are taken from the dump as they are and jq
prints null for a pin without one, that every pin has an id and a type
and carries the direction in the parent-device nest, and that an input
pin reports a valid state. The property test does not go on to query
pins by an id that is not one, and reports a pin it cannot query at all
as one failure rather than as one per attribute. The frequency is not
required, a pin without a frequency callback does not report one. The
device the script created has the standard pins, so an empty pin list
or a missing input pin on it means a failed query or broken tooling
rather than a pinless device. The library gains
dpll_log_test_skip_or_fail for such conditions, which fails on the
device the script created, recorded in DPLL_CREATED_DEVICE next to
DPLL_DEV_ID, and skips on a device the user provided. The stress script
creates its devices with a random set of pins of every type and both
directions, MUX pins always as inputs, and the deploy cycle test runs
with the standard pin set.

Signed-off-by: Petr Oros <poros@redhat.com>
---
v2:
- quote the ksft_skip and ret expansions (netdev CI shellcheck)

v1: https://lore.kernel.org/all/20261103093910.596225-5-poros@redhat.com/
---
 .../selftests/drivers/net/dpll/dpll_basic.sh  | 146 +++++++++++++-
 .../selftests/drivers/net/dpll/dpll_stress.sh |  46 ++++-
 .../selftests/drivers/net/lib/sh/dpll_lib.sh  | 189 ++++++++++++++++--
 3 files changed, 357 insertions(+), 24 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_basic.sh b/tools/testing/selftests/drivers/net/dpll/dpll_basic.sh
index 0d823fbc943a84..d83eac86551347 100755
--- a/tools/testing/selftests/drivers/net/dpll/dpll_basic.sh
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_basic.sh
@@ -1,10 +1,10 @@
 #!/bin/bash
 # SPDX-License-Identifier: GPL-2.0
 #
-# Basic DPLL subsystem test. Read-only validation of the device properties
-# exposed over netlink. Runs against a hardware DPLL device when one is
-# present (or when DPLL_DEV_ID is set), otherwise against a freshly
-# created dpllsim device.
+# Basic DPLL subsystem test. Read-only validation of device and pin
+# enumeration and of the properties exposed over netlink. Runs against a
+# hardware DPLL device when one is present (or when DPLL_DEV_ID is set),
+# otherwise against a freshly created dpllsim device.
 #
 # Copyright (c) 2026 Red Hat, Inc.
 
@@ -15,8 +15,29 @@ source "$SCRIPTDIR/../lib/sh/dpll_lib.sh"
 
 ALL_TESTS="
 	test_device_properties
+	test_pin_enumeration
+	test_pin_properties
+	test_pin_state
 "
 
+# Fail on every entry of a pin id list that is not a number. The ids are
+# taken from the dump as they are, so a pin without one shows up as the
+# "null" jq prints for a missing field, not as an empty line. Return 1
+# when any entry is invalid.
+check_pin_ids()
+{
+	local id ret=0
+
+	for id in "$@"; do
+		if ! [[ "$id" =~ ^[0-9]+$ ]]; then
+			check_err 1 "invalid pin id '$id'"
+			ret=1
+		fi
+	done
+
+	return "$ret"
+}
+
 test_device_properties()
 {
 	local json
@@ -42,6 +63,114 @@ test_device_properties()
 	log_test "DPLL device properties"
 }
 
+test_pin_enumeration()
+{
+	local pin_ids
+
+	RET=0
+
+	mapfile -t pin_ids < <(dpll_pin_get_ids "$DPLL_DEV_ID")
+	if [ "${#pin_ids[@]}" -eq 0 ]; then
+		dpll_log_test_skip_or_fail "DPLL pin enumeration" \
+		    "no pins found"
+		return
+	fi
+	check_pin_ids "${pin_ids[@]}"
+
+	log_test "DPLL pin enumeration (${#pin_ids[@]} pins)"
+}
+
+check_pin_properties()
+{
+	local pin_id=$1
+	local json field
+
+	# A pin that cannot be queried at all would be reported as one
+	# that misses every attribute, so bail out on that first.
+	if ! json=$(dpll_pin_json "$pin_id") || [ -z "$json" ]; then
+		check_err 1 "pin $pin_id query failed"
+		return
+	fi
+
+	# Frequency is optional, a pin without a frequency callback such
+	# as a recovered clock pin does not report it.
+	for field in '.id' '.type'; do
+		if ! jq -e "$field" <<< "$json" &> /dev/null; then
+			check_err 1 \
+			    "pin $pin_id is missing the $field attribute"
+		fi
+	done
+
+	# The parent-device nest is not present for all pin types, but when
+	# the device under test is in it, that entry must carry the
+	# direction. A pin can be registered on more than one device, so
+	# match on parent-id instead of taking the first entry.
+	if jq -e --argjson dev "$DPLL_DEV_ID" \
+		'[.["parent-device"][]? | select(.["parent-id"] == $dev)] |
+		 length > 0' <<< "$json" &> /dev/null; then
+		if ! jq -e --argjson dev "$DPLL_DEV_ID" \
+			'.["parent-device"][]? |
+			 select(.["parent-id"] == $dev) | .direction' \
+			<<< "$json" &> /dev/null; then
+			check_err 1 \
+			    "pin $pin_id parent-device is missing direction"
+		fi
+	fi
+}
+
+test_pin_properties()
+{
+	local pin_ids pin_id
+
+	RET=0
+
+	mapfile -t pin_ids < <(dpll_pin_get_ids "$DPLL_DEV_ID")
+	if [ "${#pin_ids[@]}" -eq 0 ]; then
+		dpll_log_test_skip_or_fail "DPLL pin properties" "no pins found"
+		return
+	fi
+	if ! check_pin_ids "${pin_ids[@]}"; then
+		log_test "DPLL pin properties"
+		return
+	fi
+
+	for pin_id in "${pin_ids[@]}"; do
+		check_pin_properties "$pin_id"
+	done
+
+	log_test "DPLL pin properties (${#pin_ids[@]} pins)"
+}
+
+test_pin_state()
+{
+	local pin_id state
+
+	RET=0
+
+	pin_id=$(dpll_pin_find_by_direction "$DPLL_DEV_ID" input | head -1)
+	if [ -z "$pin_id" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin state" "no input pin"
+		return
+	fi
+
+	# The pin was found through its parent-device nest for this device
+	# and the state is mandatory there, so an empty value is a failed
+	# query rather than a state.
+	state=$(dpll_pin_get_state "$DPLL_DEV_ID" "$pin_id")
+	case "$state" in
+	"")
+		check_err 1 "cannot read the state of pin $pin_id"
+		;;
+	connected|disconnected|selectable)
+		;;
+	*)
+		check_err 1 "pin $pin_id reports invalid state '$state'"
+		;;
+	esac
+
+	log_test "DPLL pin state"
+}
+
 ##############################################################################
 
 cleanup()
@@ -49,9 +178,9 @@ cleanup()
 	# A second signal must not abort the teardown half way.
 	trap '' INT TERM
 
-	if [ -n "$CREATED_DEVICE" ]; then
-		dpllsim_delete_device "$CREATED_DEVICE" ||
-			log_info "cannot delete device $CREATED_DEVICE"
+	if [ -n "$DPLL_CREATED_DEVICE" ]; then
+		dpllsim_delete_device "$DPLL_CREATED_DEVICE" ||
+			log_info "cannot delete device $DPLL_CREATED_DEVICE"
 	fi
 	# The device whose creation a signal interrupted is recorded by the
 	# library alone, a double delete of a tracked one is harmless.
@@ -68,7 +197,6 @@ dpll_lib_check_prereqs
 dpll_lib_lock
 dpll_lib_init
 
-CREATED_DEVICE=""
 # The module check may mount configfs, so the exit trap that unmounts it
 # again has to be in place before. INT and TERM exit through it as well,
 # so that a timeout imposed by the harness still removes the device.
@@ -81,8 +209,8 @@ if [ -z "$DPLL_DEV_ID" ]; then
 		echo "SKIP: cannot create dpllsim device"
 		exit "$ksft_skip"
 	fi
-	CREATED_DEVICE=$DPLLSIM_CREATED_NAME
 	DPLL_DEV_ID=$DPLLSIM_DEV_ID
+	DPLL_CREATED_DEVICE=$DPLLSIM_CREATED_NAME
 	DPLL_MODULE_NAME="dpllsim"
 fi
 
diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
index 74bb6e8b3a8ed4..e5a1c4d88da580 100755
--- a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
@@ -58,15 +58,47 @@ cleanup()
 	dpll_lib_cleanup
 }
 
-# Create a device named after the given clock-id and deploy it. Sets
-# DPLLSIM_CREATED_NAME.
+# Pin type and a frequency the simulator supports for it.
+STRESS_PIN_KINDS=(
+	"gnss 1"
+	"ext 10000000"
+	"ext 25000000"
+	"synce-eth-port 10000000"
+	"int-oscillator 25000000"
+	"mux 2048000"
+)
+
+# Create a device named after the given clock-id with the given pin count
+# and deploy it. Sets DPLLSIM_CREATED_NAME.
 create_stress_device()
 {
 	local clock_id=$1
+	local num_pins=$2
 	local name="st$clock_id"
+	local i type freq dir
+	local dirs=(input output)
 
 	dpllsim_create_device "$name" "$clock_id" || return 1
 
+	# Always have at least one plain input pin, then random ones
+	if ! dpllsim_add_pin "$name" ext0 ext 10000000 input; then
+		dpllsim_delete_device "$name"
+		return 1
+	fi
+	for i in $(seq 1 $((num_pins - 1))); do
+		read -r type freq <<< \
+			"${STRESS_PIN_KINDS[RANDOM % ${#STRESS_PIN_KINDS[@]}]}"
+		# A MUX pin is an input selector, the simulator rejects an
+		# output one
+		dir=${dirs[RANDOM % 2]}
+		[ "$type" = mux ] && dir=input
+		if ! dpllsim_add_pin "$name" "pin$i" "$type" "$freq" \
+				     "$dir"; then
+			dpllsim_delete_device "$name"
+			return 1
+		fi
+	done
+
 	if ! dpllsim_deploy "$name"; then
 		dpllsim_delete_device "$name"
 		return 1
@@ -84,7 +116,7 @@ test_rapid_create_delete()
 	for i in $(seq 1 "$STRESS_ITERATIONS"); do
 		dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
 
-		if ! create_stress_device "$clock_id"; then
+		if ! create_stress_device "$clock_id" 5; then
 			check_err 1 "failed to create device (iteration $i)"
 			break
 		fi
@@ -116,6 +148,14 @@ test_deploy_cycle()
 	fi
 	track_device "$name"
 
+	if ! dpllsim_add_pin "$name" gnss0 gnss 1 input ||
+	   ! dpllsim_add_pin "$name" ext0 ext 10000000 input ||
+	   ! dpllsim_add_pin "$name" out0 ext 10000000 output; then
+		check_err 1 "cannot add pins"
+		log_test "DPLL deploy/undeploy cycling"
+		return
+	fi
+
 	for i in $(seq 1 "$STRESS_ITERATIONS"); do
 		if ! dpllsim_deploy "$name"; then
 			check_err 1 "deploy failed (iteration $i)"
diff --git a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
index fc81f149d1d0b7..ade1ac65f98b46 100644
--- a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
@@ -44,6 +44,10 @@ dpll_lib_check_prereqs()
 		echo "SKIP: dpll tool too old, missing 'device show'"
 		exit "$ksft_skip"
 	fi
+	if ! dpll pin help 2>&1 | grep -q "show"; then
+		echo "SKIP: dpll tool too old, missing 'pin show'"
+		exit "$ksft_skip"
+	fi
 }
 
 # The scripts that create dpllsim devices share the one dpllsim directory
@@ -108,14 +112,39 @@ dpll_lib_init()
 	fi
 }
 
+# Name of the dpllsim device a script created as the device under test,
+# set by the script next to DPLL_DEV_ID. Empty when the user provided
+# the device.
+DPLL_CREATED_DEVICE=""
+
+# The layout and the capabilities of the device a script created are
+# known, so a missing pin, attribute or capability on it can only mean a
+# failed query or broken tooling. Report such a condition as a failure
+# on the created device and as a skip on a device the user provided,
+# where it is a property of the hardware.
+# $1: test name, $2: reason
+dpll_log_test_skip_or_fail()
+{
+	local name=$1
+	local reason=$2
+
+	if [ -n "$DPLL_CREATED_DEVICE" ]; then
+		check_err 1 "$reason on the device created by the test"
+		log_test "$name"
+	else
+		log_test_skip "$name" "$reason"
+	fi
+}
+
 ##############################################################################
 # Device queries
 #
 # "dpll device show id X -j" may print either a flat object or a dump-style
 # {"device":[...]} wrapper depending on the iproute2 version, so normalize
-# with (.device[0]? // .) before extracting fields. The tool is run first
-# and jq only on what it printed, so that a failed query returns its
-# status instead of the empty output a missing field produces too.
+# with (.device[0]? // .) before extracting fields, same for pins. The
+# device helpers run the tool first and jq only on what it printed, so
+# that a failed query returns its status instead of the empty output a
+# missing field produces too.
 
 dpll_device_json()
 {
@@ -197,12 +226,110 @@ dpll_device_wait_gone_by_clock_id()
 	busywait "$BUSYWAIT_TIMEOUT" __dpll_device_absent_by_clock_id "$@"
 }
 
+##############################################################################
+# Pin queries
+#
+# Attributes of the pin itself are read by pin id alone. Attributes that
+# live in the parent-device nest (state, prio, ...) take the device id in
+# addition, to select the nest of the device under test. The helpers
+# that work on the full pin dump normalize its shape like the id-based
+# ones do, with (.pin? // [.]), so that a flat object would work too.
+# Like the device queries, every helper runs the tool first and jq only
+# on what it printed, and returns 1 when the tool failed.
+
+dpll_pin_json()
+{
+	local pin_id=$1
+	local out
+
+	out=$(dpll pin show id "$pin_id" -j 2>/dev/null) || return 1
+	jq '(.pin[0]? // .)' <<< "$out"
+}
+
+__dpll_pin_field()
+{
+	local pin_id=$1; shift
+	local out
+
+	out=$(dpll pin show id "$pin_id" -j 2>/dev/null) || return 1
+	jq -r "(.pin[0]? // .) | $1 // empty" <<< "$out"
+}
+
+# Fields living in the parent-device nest matching the given device
+__dpll_pin_dev_field()
+{
+	local dev_id=$1
+	local pin_id=$2; shift 2
+	local out
+
+	out=$(dpll pin show id "$pin_id" -j 2>/dev/null) || return 1
+	jq -r --argjson dev "$dev_id" \
+	   "(.pin[0]? // .) | .[\"parent-device\"][]? |
+	    select(.[\"parent-id\"] == \$dev) | $1 // empty" <<< "$out"
+}
+
+# Print ids of all pins belonging to the device, one per line: pins
+# registered on the device directly plus pins registered on those pins
+# (MUX children), followed recursively so that children of children are
+# included too. Older iproute2 lacks the "pin show device <id>" filter,
+# so filter the full dump with jq.
+dpll_pin_get_ids()
+{
+	local dev_id=$1
+	local out
+
+	out=$(dpll pin show -j 2>/dev/null) || return 1
+	jq -r --argjson dev "$dev_id" '
+		(.pin? // [.]) as $pins |
+		def children($ids):
+			[$pins[] | select(any(.["parent-pin"][]?;
+					     .["parent-id"] as $p |
+					     $ids | index($p))) | .id];
+		def closure($ids):
+			(($ids + children($ids)) | unique) as $n |
+			if ($n | length) == ($ids | length) then $ids
+			else closure($n) end;
+		closure([$pins[] |
+			 select(any(.["parent-device"][]?;
+				    .["parent-id"] == $dev)) | .id])
+		as $all |
+		$pins[] | select(.id as $i | $all | index($i)) | .id' \
+		<<< "$out"
+}
+
+# Print ids of all non-MUX pins of the device with the given direction
+# (input/output), one per line. Direction lives in the parent-device nest.
+dpll_pin_find_by_direction()
+{
+	local dev_id=$1
+	local dir=$2
+	local out
+
+	out=$(dpll pin show -j 2>/dev/null) || return 1
+	jq -r --argjson dev "$dev_id" --arg dir "$dir" \
+	   '(.pin? // [.])[] | select(.type != "mux") |
+	    select(.["parent-device"][]? |
+		   (.["parent-id"] == $dev and .direction == $dir)) |
+	    .id' <<< "$out"
+}
+
+dpll_pin_get_frequency()
+{
+	__dpll_pin_field "$1" '.frequency'
+}
+
+dpll_pin_get_state()
+{
+	__dpll_pin_dev_field "$1" "$2" '.state'
+}
+
 ##############################################################################
 # dpllsim configfs interface
 #
-# Each directory under $DPLLSIM_CFG is one simulated device. Writing 1 to
-# <dev>/live registers the device with the DPLL core, writing 0
-# unregisters it. The structure (clock_id) can only be changed while the
+# Each directory under $DPLLSIM_CFG is one simulated device; its pins are
+# directories under <dev>/pins/. Writing 1 to <dev>/live registers the
+# device and its pins with the DPLL core, writing 0 unregisters them. The
+# structure (pins, clock_id, pin attributes) can only be changed while the
 # device is not live.
 #
 # dpllsim_available mounts configfs when nothing else did, which is the
@@ -275,6 +402,28 @@ dpllsim_create_device()
 	fi
 }
 
+# $1: device name, $2: pin name (becomes the board-label), $3: type (gnss,
+# int-oscillator, ext, synce-eth-port, mux), $4: frequency in Hz,
+# $5: direction (input/output)
+dpllsim_add_pin()
+{
+	local dev=$1
+	local pin=$2
+	local type=$3
+	local frequency=$4
+	local direction=$5
+	local pin_path="$DPLLSIM_CFG/$dev/pins/$pin"
+
+	mkdir "$pin_path" 2>/dev/null || return 1
+
+	if ! echo "$type" 2>/dev/null > "$pin_path/type" ||
+	   ! echo "$frequency" 2>/dev/null > "$pin_path/frequency" ||
+	   ! echo "$direction" 2>/dev/null > "$pin_path/direction"; then
+		rmdir "$pin_path" 2>/dev/null
+		return 1
+	fi
+}
+
 dpllsim_deploy()
 {
 	echo 1 2>/dev/null > "$DPLLSIM_CFG/$1/live"
@@ -285,17 +434,22 @@ dpllsim_undeploy()
 	echo 0 2>/dev/null > "$DPLLSIM_CFG/$1/live"
 }
 
-# Take the device offline and remove it. Every step is attempted even
-# when an earlier one failed, the return value is non-zero when any did.
-# A device that does not exist counts as deleted.
+# Take the device offline and remove it together with all its pins.
+# Every step is attempted even when an earlier one failed, the return
+# value is non-zero when any did. A device that does not exist counts as
+# deleted.
 dpllsim_delete_device()
 {
 	local dev_path="$DPLLSIM_CFG/$1"
-	local ret=0
+	local pin_path ret=0
 
 	[ -d "$dev_path" ] || return 0
 
 	dpllsim_undeploy "$1" || ret=1
+	for pin_path in "$dev_path"/pins/*/; do
+		[ -d "$pin_path" ] || continue
+		rmdir "$pin_path" || ret=1
+	done
 	rmdir "$dev_path" || ret=1
 
 	return "$ret"
@@ -309,8 +463,12 @@ dpllsim_alloc_clock_id()
 	DPLLSIM_CLOCK_ID=$((DPLLSIM_NEXT_CLOCK_ID++))
 }
 
-# Create a device, deploy it and resolve the netlink device id. Sets
-# DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and DPLLSIM_DEV_ID.
+# Create a device with the standard 3-pin layout, deploy it and resolve
+# the netlink device id. Sets DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and
+# DPLLSIM_DEV_ID.
+#   gnss0: GNSS, 1 Hz, input
+#   ext0: EXT, 10 MHz, input
+#   out0: EXT, 10 MHz, output
 # shellcheck disable=SC2120 # the argument is optional
 dpllsim_setup_standard_device()
 {
@@ -321,6 +479,13 @@ dpllsim_setup_standard_device()
 	name="std$clock_id"
 	dpllsim_create_device "$name" "$clock_id" || return 1
 
+	if ! dpllsim_add_pin "$name" gnss0 gnss 1 input ||
+	   ! dpllsim_add_pin "$name" ext0 ext 10000000 input ||
+	   ! dpllsim_add_pin "$name" out0 ext 10000000 output; then
+		dpllsim_delete_device "$name"
+		return 1
+	fi
+
 	if ! dpllsim_deploy "$name"; then
 		dpllsim_delete_device "$name"
 		return 1
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 05/14] dpll: dpllsim: Add support to set input pin state
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (3 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 04/14] selftests: dpll: add pin enumeration and property tests Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:41   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 06/14] selftests: dpll: add pin state tests Petr Oros
                   ` (9 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Add support for setting the state of input pins on the device.
Implement the state_on_dpll_set callback for input pins and advertise
the state-can-change capability for them, so that userspace can move a
pin between connected, disconnected and selectable. The driver stores
the requested state and reports it back unchanged, it does not act on it
yet. Output pins keep reporting their state only.

Signed-off-by: Petr Oros <poros@redhat.com>
---
 drivers/dpll/dpllsim/dpll.c | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index a1d5921fb8d017..2b0c053a7b6087 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -104,10 +104,23 @@ static int dpllsim_pin_state_get(const struct dpll_pin *pin, void *pin_priv,
 	return 0;
 }
 
+static int dpllsim_pin_state_set(const struct dpll_pin *pin, void *pin_priv,
+				 const struct dpll_device *dpll,
+				 void *dpll_priv, enum dpll_pin_state state,
+				 struct netlink_ext_ack *extack)
+{
+	struct dpllsim_pin *sim_pin = pin_priv;
+
+	guard(mutex)(&sim_pin->dev->lock);
+	sim_pin->state = state;
+	return 0;
+}
+
 static const struct dpll_pin_ops dpllsim_input_pin_ops = {
 	.frequency_get = dpllsim_pin_frequency_get,
 	.direction_get = dpllsim_pin_direction_get,
 	.state_on_dpll_get = dpllsim_pin_state_get,
+	.state_on_dpll_set = dpllsim_pin_state_set,
 };
 
 static const struct dpll_pin_ops dpllsim_output_pin_ops = {
@@ -149,6 +162,12 @@ static void dpllsim_pin_props(struct dpllsim_pin *pin,
 		prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_ext);
 		break;
 	}
+
+	/* Capabilities follow the ops the pin is served by */
+	if (dpllsim_pin_select_ops(pin) == &dpllsim_input_pin_ops)
+		prop->capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+	else
+		prop->capabilities = 0;
 }
 
 static bool dpllsim_pin_frequency_valid(struct dpllsim_pin *pin)
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 06/14] selftests: dpll: add pin state tests
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (4 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 05/14] dpll: dpllsim: Add support to set input pin state Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:49   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 07/14] dpll: dpllsim: Add support to get/set priority on input pins Petr Oros
                   ` (8 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Add dpll_features.sh, a script that exercises individual device and pin
callbacks with get/set roundtrips over netlink. It creates a dpllsim
device with the standard pin layout by default or runs against the
device named by DPLL_DEV_ID, and picks the pin to reconfigure up front,
the first input pin that is not connected on the device, so that no
test touches the reference the device is locked to. The first test
moves that pin between the disconnected and selectable states, reads
the state back after each step and restores the original state at the
end. It never connects the pin, which could make it the reference.

A driver may refuse a state the capability promises, for a constraint
the interface does not tell. That is a skip on a device provided by
the user, with the reason the tool reported, and a failure on the
device the script created, whose simulator accepts both states. The
restore is deferred right after the original state is read, and the
cleanup handler of every script that defers runs the pending restores
before it removes the devices the script created, so that an
interrupted run puts the device back as well.

The stress script gains a test that floods one input pin of a fresh
device with state changes. The library gains the state setter both
use, which keeps the extack line of the tool in DPLL_LAST_ERR so that
a test can tell a refusal from a failure, and a check whether the pin
set usage of the tool knows a keyword, so that a test skips on a tool
too old for it rather than every script requiring a newer tool.

Signed-off-by: Petr Oros <poros@redhat.com>
---
v2:
- quote the ksft_skip expansion (netdev CI shellcheck)

v1: https://lore.kernel.org/all/20261103093910.596225-7-poros@redhat.com/
---
 .../selftests/drivers/net/dpll/Makefile       |   1 +
 .../drivers/net/dpll/dpll_features.sh         | 183 ++++++++++++++++++
 .../selftests/drivers/net/dpll/dpll_stress.sh |  57 ++++++
 .../selftests/drivers/net/lib/sh/dpll_lib.sh  |  76 +++++++-
 4 files changed, 316 insertions(+), 1 deletion(-)
 create mode 100755 tools/testing/selftests/drivers/net/dpll/dpll_features.sh

diff --git a/tools/testing/selftests/drivers/net/dpll/Makefile b/tools/testing/selftests/drivers/net/dpll/Makefile
index 7752fb518b6e9b..7cb42c93b0c9a1 100644
--- a/tools/testing/selftests/drivers/net/dpll/Makefile
+++ b/tools/testing/selftests/drivers/net/dpll/Makefile
@@ -8,6 +8,7 @@ TEST_INCLUDES := \
 
 TEST_PROGS := \
 	dpll_basic.sh \
+	dpll_features.sh \
 	dpll_stress.sh \
 # end of TEST_PROGS
 
diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_features.sh b/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
new file mode 100755
index 00000000000000..c73417a058e0a0
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
@@ -0,0 +1,183 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# DPLL device and pin feature tests. Exercises individual dpll_device_ops
+# and dpll_pin_ops callbacks, verifying get/set roundtrips through the
+# netlink interface. By default a dpllsim device with a known pin layout
+# is created; set DPLL_DEV_ID to run against an existing device instead,
+# the simulator-only tests then skip unless that device is a dpllsim one.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../../net/lib.sh"
+source "$SCRIPTDIR/../lib/sh/dpll_lib.sh"
+
+ALL_TESTS="
+	test_pin_state_transitions
+"
+
+##############################################################################
+# Pin-level feature tests
+
+test_pin_state_transitions()
+{
+	local before state after reason
+
+	RET=0
+
+	if [ -z "$PIN_TUNE_ID" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin state transitions" \
+		    "no spare input pin"
+		return
+	fi
+	if ! dpll_pin_set_supports state; then
+		log_test_skip "DPLL pin state transitions" \
+		    "dpll tool lacks pin set state"
+		return
+	fi
+	if ! dpll_pin_has_capability "$PIN_TUNE_ID" state-can-change; then
+		dpll_log_test_skip_or_fail "DPLL pin state transitions" \
+		    "state cannot change"
+		return
+	fi
+
+	before=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+	if [ -z "$before" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin state transitions" \
+		    "state not reported"
+		return
+	fi
+	# Restore the original state even when the test aborts half way.
+	defer dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"
+
+	# The pin is not connected, see how PIN_TUNE_ID is picked, and the
+	# test never connects it, it moves the pin between the disconnected
+	# and selectable states only and ends at the original one. A pin
+	# that is connected may become the reference the device locks to.
+	#
+	# A driver may refuse a state the capability promises, for a
+	# constraint the interface does not tell. On a device provided by
+	# the user that is a skip with the reason the tool reported, on
+	# the created device a failure, the simulator accepts both states.
+	# The test ends there, whichever set was refused, a state that was
+	# not set has nothing to read back and the deferred restore puts
+	# the pin back. A refused restore is treated the same way when
+	# nothing failed before it, afterwards it is one more failure, a
+	# skip would hide the earlier one.
+	for state in disconnected selectable; do
+		if ! dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" \
+					"$state"; then
+			reason=${DPLL_LAST_ERR%%$'\n'*}
+			dpll_log_test_skip_or_fail \
+			    "DPLL pin state transitions" \
+			    "cannot set pin state to $state${reason:+, $reason}"
+			return
+		fi
+		after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+		if [ "$after" != "$state" ]; then
+			check_err 1 "pin not $state, state '$after'"
+		fi
+	done
+
+	if ! dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"; then
+		reason=${DPLL_LAST_ERR%%$'\n'*}
+		reason=${reason:+, $reason}
+		if [ "$RET" -eq 0 ]; then
+			dpll_log_test_skip_or_fail \
+			    "DPLL pin state transitions" \
+			    "cannot set pin state to $before$reason"
+			return
+		fi
+		check_err 1 "cannot restore pin state$reason"
+	fi
+	after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+	if [ "$after" != "$before" ]; then
+		check_err 1 "pin state not restored, got '$after'"
+	fi
+
+	log_test "DPLL pin state transitions"
+}
+
+##############################################################################
+
+cleanup()
+{
+	# A second signal must not abort the teardown half way.
+	trap '' INT TERM
+
+	# Run the pending restores of an interrupted test first, the device
+	# they apply to may be one the user provided.
+	defer_scopes_cleanup
+	if [ -n "$DPLL_CREATED_DEVICE" ]; then
+		dpllsim_delete_device "$DPLL_CREATED_DEVICE" ||
+			log_info "cannot delete device $DPLL_CREATED_DEVICE"
+	fi
+	# The device whose creation a signal interrupted is recorded by the
+	# library alone, a double delete of a tracked one is harmless.
+	if [ -n "$DPLLSIM_CREATED_NAME" ]; then
+		dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
+			log_info "cannot delete device $DPLLSIM_CREATED_NAME"
+	fi
+	dpll_lib_cleanup
+}
+
+dpll_lib_check_prereqs
+# Another run would find the devices of this one left behind, see the
+# lock in the library.
+dpll_lib_lock
+
+# The module check may mount configfs, so the exit trap that unmounts it
+# again has to be in place before. INT and TERM exit through it as well,
+# so that a timeout imposed by the harness still restores the device.
+trap cleanup EXIT
+trap 'exit 1' INT TERM
+
+if [ -n "${DPLL_DEV_ID:-}" ]; then
+	dpll_lib_init
+else
+	dpllsim_check_module
+	if ! dpllsim_setup_standard_device; then
+		echo "SKIP: cannot create dpllsim device"
+		exit "$ksft_skip"
+	fi
+	DPLL_DEV_ID=$DPLLSIM_DEV_ID
+	DPLL_CREATED_DEVICE=$DPLLSIM_CREATED_NAME
+	DPLL_MODULE_NAME="dpllsim"
+fi
+
+log_info "testing DPLL device $DPLL_DEV_ID (module $DPLL_MODULE_NAME)"
+
+# PIN_TUNE_ID is the input pin the tests reconfigure. It must not be the
+# reference the device is locked to, so it is the first input pin that
+# is not connected on the device. When the first dumped input pin and
+# another one both qualify the other one is taken, so that the first
+# pin, the one a simulated device locks to once it is connected, is left
+# alone. Tests that need the pin skip when there is none rather than
+# reconfiguring the reference.
+pick_tune_pin()
+{
+	local pin_id state
+
+	PIN_TUNE_ID=""
+	for pin_id in "${input_pins[@]}"; do
+		state=$(dpll_pin_get_state "$DPLL_DEV_ID" "$pin_id")
+		case "$state" in
+		disconnected|selectable)
+			;;
+		*)
+			continue
+			;;
+		esac
+		PIN_TUNE_ID=$pin_id
+		[ "$pin_id" != "${input_pins[0]}" ] && return
+	done
+}
+
+mapfile -t input_pins < <(dpll_pin_find_by_direction "$DPLL_DEV_ID" input)
+pick_tune_pin
+
+tests_run
+
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
index e5a1c4d88da580..ee543e2cd8703c 100755
--- a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
@@ -15,6 +15,7 @@ source "$SCRIPTDIR/../lib/sh/dpll_lib.sh"
 
 ALL_TESTS="
 	test_rapid_create_delete
+	test_pin_state_flooding
 	test_deploy_cycle
 "
 
@@ -45,6 +46,7 @@ cleanup()
 	# A second signal must not abort the teardown half way.
 	trap '' INT TERM
 
+	defer_scopes_cleanup
 	for dev in "${CREATED_DEVICES[@]}"; do
 		dpllsim_delete_device "$dev" ||
 			log_info "cannot delete device $dev"
@@ -133,6 +135,61 @@ test_rapid_create_delete()
 	log_test "DPLL rapid create/delete ($STRESS_ITERATIONS iterations)"
 }
 
+test_pin_state_flooding()
+{
+	local clock_id name dev_id pin_id i last state fails=0
+	local states=(connected disconnected selectable)
+
+	RET=0
+
+	if ! dpll_pin_set_supports state; then
+		log_test_skip "DPLL pin state flooding" \
+			      "dpll tool lacks pin set state"
+		return
+	fi
+
+	dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
+	if ! create_stress_device "$clock_id" 10; then
+		check_err 1 "cannot create device"
+		log_test "DPLL pin state flooding"
+		return
+	fi
+	name=$DPLLSIM_CREATED_NAME
+	track_device "$name"
+
+	if ! dev_id=$(dpll_device_wait_by_clock_id "$clock_id" dpllsim); then
+		check_err 1 "device not visible on netlink"
+	else
+		pin_id=$(dpll_pin_find_by_direction "$dev_id" input | head -1)
+		if [ -z "$pin_id" ]; then
+			check_err 1 "no input pin on stress device"
+		else
+			for i in $(seq 1 50); do
+				last=${states[i % 3]}
+				dpll_pin_set_state "$dev_id" "$pin_id" \
+						   "$last" ||
+					fails=$((fails + 1))
+			done
+			if [ "$fails" -gt 0 ]; then
+				check_err 1 "$fails of 50 state changes failed"
+			fi
+			state=$(dpll_pin_get_state "$dev_id" "$pin_id")
+			if [ "$state" != "$last" ]; then
+				check_err 1 \
+				    "state expected '$last', got '$state'"
+			fi
+		fi
+	fi
+
+	if dpllsim_delete_device "$name"; then
+		untrack_device "$name"
+	else
+		check_err 1 "cannot delete device $name"
+	fi
+
+	log_test "DPLL pin state flooding"
+}
+
 test_deploy_cycle()
 {
 	local clock_id name i
diff --git a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
index ade1ac65f98b46..ee91ad1722971d 100644
--- a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
@@ -5,7 +5,8 @@
 # tool and around the dpllsim configfs interface for creating virtual DPLL
 # devices. Sourcing this file has no side effects; call
 # dpll_lib_check_prereqs and dpll_lib_init explicitly. net/lib.sh must be
-# sourced first, it provides ksft_skip, busywait and log_info.
+# sourced first, it provides ksft_skip, busywait and log_info to the
+# library and defer to the scripts.
 #
 # Copyright (c) 2026 Red Hat, Inc.
 #
@@ -86,6 +87,44 @@ dpll_lib_lock()
 	fi
 }
 
+# Print the usage block of the given command of the given object from
+# the help of the dpll tool, "dpll pin set" for instance. The help lists
+# one usage per command, each starting with "Usage: dpll <obj> <cmd>" or
+# with "dpll <obj> <cmd>" indented under the first, possibly wrapped
+# over further lines. The block starts at the line that names the
+# command, with a space after the name so that a command whose name
+# merely begins the same way does not match, and ends before the next
+# line that names another command of the object, or at the end of the
+# help when none follows. Return 1 when the help has no usage for the
+# command, in which case nothing is printed.
+__dpll_usage_block()
+{
+	local obj=$1
+	local cmd=$2
+
+	dpll "$obj" help 2>&1 | \
+		awk -v start="[[:space:]]dpll $obj $cmd([[:space:]]|$)" \
+		    -v other="[[:space:]]dpll $obj [a-z-]+([[:space:]]|$)" \
+		    '$0 ~ start { s = 1; print; next }
+		     s && $0 ~ other { exit }
+		     s { print }
+		     END { exit !s }'
+}
+
+# Return 0 when the dpll tool knows the given "pin set" keyword, such as
+# state or frequency. A test that sets something checks its keyword
+# first and skips on a tool that is too old for it. Only the "pin set"
+# usage is searched, a keyword that only "pin show" knows must not
+# count, and the keyword must stand on its own between blanks or
+# brackets, a hyphenated compound that contains it does not count.
+dpll_pin_set_supports()
+{
+	local usage
+
+	usage=$(__dpll_usage_block pin set) || return 1
+	grep -qE "(^|[[:space:][])$1([[:space:]]|$)" <<< "$usage"
+}
+
 # Detect the DPLL device to test. DPLL_DEV_ID may be set by the user to
 # force a specific device; an empty value means auto-detect, which takes
 # the first device that is not a dpllsim one, so that an instance of the
@@ -323,6 +362,41 @@ dpll_pin_get_state()
 	__dpll_pin_dev_field "$1" "$2" '.state'
 }
 
+# Return 0 when the pin advertises the given capability, named as
+# iproute2 prints it (state-can-change, priority-can-change, ...).
+dpll_pin_has_capability()
+{
+	local pin_id=$1
+	local name=$2
+	local out
+
+	out=$(dpll pin show id "$pin_id" -j 2>/dev/null) || return 1
+	jq -e --arg name "$name" \
+	   '(.pin[0]? // .) | .capabilities | index($name) != null' \
+	   <<< "$out" &> /dev/null
+}
+
+##############################################################################
+# Pin setters
+#
+# Every setter returns the status of the tool and keeps what the tool
+# printed on stderr in DPLL_LAST_ERR, the "Error: ..." line that carries
+# the extack message of the kernel, so that a caller can tell a refusal
+# it may skip on from a failure.
+
+DPLL_LAST_ERR=""
+
+# $3: connected / disconnected / selectable
+dpll_pin_set_state()
+{
+	local dev_id=$1
+	local pin_id=$2
+	local state=$3
+
+	DPLL_LAST_ERR=$(dpll pin set id "$pin_id" parent-device "$dev_id" \
+			     state "$state" 2>&1 >/dev/null)
+}
+
 ##############################################################################
 # dpllsim configfs interface
 #
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 07/14] dpll: dpllsim: Add support to get/set priority on input pins
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (5 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 06/14] selftests: dpll: add pin state tests Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:49   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 08/14] selftests: dpll: add pin priority test Petr Oros
                   ` (7 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Add support for getting and setting the priority of input pins.
Implement the prio_get and prio_set callbacks and advertise the
priority-can-change capability for input pins, which includes the MUX
type pins since those are always inputs.

The initial priority of a pin is configured through a new prio
attribute of the pin directory, which takes a value between 0 and 255
and defaults to 128, so that a test can lay out the reference order
before the device goes live. Like the other pin attributes it is
writable only while the device is not live. Priority changes made over
netlink are stored and reported back, they will drive the automatic
reference selection once the simulation loop exists. The setter
rejects a value above 255 with EINVAL and an extack message. A priority
is meaningful for input pins only, so live fails with EINVAL when an
output pin was given a priority other than the default, next to the
frequency validation.

Signed-off-by: Petr Oros <poros@redhat.com>
---
 Documentation/driver-api/dpllsim.rst |  5 ++++
 drivers/dpll/dpllsim/core.c          | 31 +++++++++++++++++++
 drivers/dpll/dpllsim/dpll.c          | 45 ++++++++++++++++++++++++++--
 drivers/dpll/dpllsim/dpllsim.h       | 10 ++++++-
 4 files changed, 88 insertions(+), 3 deletions(-)

diff --git a/Documentation/driver-api/dpllsim.rst b/Documentation/driver-api/dpllsim.rst
index e7c5ee93392c21..5109d67325f140 100644
--- a/Documentation/driver-api/dpllsim.rst
+++ b/Documentation/driver-api/dpllsim.rst
@@ -36,6 +36,9 @@ Pin directory ``/sys/kernel/config/dpllsim/<dev>/pins/<pin>/``:
                (default 0, which is rejected)
 ``direction``  ``input`` or ``output`` (default ``input``), a ``mux`` pin
                must be an input
+``prio``       Priority 0-255 used for automatic reference selection,
+               input pins only, output pins must keep the default
+               (default 128)
 =============  ===============================================================
 
 The pin directory name becomes the board label of the pin. Pins are
@@ -53,8 +56,10 @@ output, then make it live::
     mkdir pins/gnss0
     echo gnss > pins/gnss0/type
     echo 1 > pins/gnss0/frequency
+    echo 0 > pins/gnss0/prio
     mkdir pins/ext0
     echo 10000000 > pins/ext0/frequency
+    echo 1 > pins/ext0/prio
     mkdir pins/out0
     echo 10000000 > pins/out0/frequency
     echo output > pins/out0/direction
diff --git a/drivers/dpll/dpllsim/core.c b/drivers/dpll/dpllsim/core.c
index 97b3cf48772e7f..ee728a7f0c21bc 100644
--- a/drivers/dpll/dpllsim/core.c
+++ b/drivers/dpll/dpllsim/core.c
@@ -132,10 +132,40 @@ static ssize_t dpllsim_pin_frequency_store(struct config_item *item,
 }
 CONFIGFS_ATTR(dpllsim_pin_, frequency);
 
+static ssize_t dpllsim_pin_prio_show(struct config_item *item, char *page)
+{
+	struct dpllsim_pin *pin = to_dpllsim_pin(item);
+
+	guard(mutex)(&pin->dev->lock);
+	return sysfs_emit(page, "%u\n", pin->prio);
+}
+
+static ssize_t dpllsim_pin_prio_store(struct config_item *item,
+				      const char *page, size_t count)
+{
+	struct dpllsim_pin *pin = to_dpllsim_pin(item);
+	u32 val;
+	int err;
+
+	err = kstrtou32(page, 10, &val);
+	if (err)
+		return err;
+	if (val > DPLLSIM_PIN_PRIO_MAX)
+		return -EINVAL;
+
+	guard(mutex)(&pin->dev->lock);
+	if (dpllsim_dev_frozen(pin->dev))
+		return -EBUSY;
+	pin->prio = val;
+	return count;
+}
+CONFIGFS_ATTR(dpllsim_pin_, prio);
+
 static struct configfs_attribute *dpllsim_pin_attrs[] = {
 	&dpllsim_pin_attr_type,
 	&dpllsim_pin_attr_direction,
 	&dpllsim_pin_attr_frequency,
+	&dpllsim_pin_attr_prio,
 	NULL,
 };
 
@@ -178,6 +208,7 @@ static struct config_item *dpllsim_pins_make_item(struct config_group *group,
 	pin->dev = sim;
 	pin->type = DPLL_PIN_TYPE_EXT;
 	pin->direction = DPLL_PIN_DIRECTION_INPUT;
+	pin->prio = DPLLSIM_DEFAULT_PIN_PRIO;
 	list_add_tail(&pin->list, &sim->pin_list);
 
 	return &pin->item;
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index 2b0c053a7b6087..8a302c2537c880 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -116,11 +116,43 @@ static int dpllsim_pin_state_set(const struct dpll_pin *pin, void *pin_priv,
 	return 0;
 }
 
+static int dpllsim_pin_prio_get(const struct dpll_pin *pin, void *pin_priv,
+				const struct dpll_device *dpll,
+				void *dpll_priv, u32 *prio,
+				struct netlink_ext_ack *extack)
+{
+	struct dpllsim_pin *sim_pin = pin_priv;
+
+	guard(mutex)(&sim_pin->dev->lock);
+	*prio = sim_pin->prio;
+	return 0;
+}
+
+static int dpllsim_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
+				const struct dpll_device *dpll,
+				void *dpll_priv, u32 prio,
+				struct netlink_ext_ack *extack)
+{
+	struct dpllsim_pin *sim_pin = pin_priv;
+
+	if (prio > DPLLSIM_PIN_PRIO_MAX) {
+		NL_SET_ERR_MSG_FMT(extack, "Pin priority must be in range 0-%u",
+				   DPLLSIM_PIN_PRIO_MAX);
+		return -EINVAL;
+	}
+
+	guard(mutex)(&sim_pin->dev->lock);
+	sim_pin->prio = prio;
+	return 0;
+}
+
 static const struct dpll_pin_ops dpllsim_input_pin_ops = {
 	.frequency_get = dpllsim_pin_frequency_get,
 	.direction_get = dpllsim_pin_direction_get,
 	.state_on_dpll_get = dpllsim_pin_state_get,
 	.state_on_dpll_set = dpllsim_pin_state_set,
+	.prio_get = dpllsim_pin_prio_get,
+	.prio_set = dpllsim_pin_prio_set,
 };
 
 static const struct dpll_pin_ops dpllsim_output_pin_ops = {
@@ -165,7 +197,8 @@ static void dpllsim_pin_props(struct dpllsim_pin *pin,
 
 	/* Capabilities follow the ops the pin is served by */
 	if (dpllsim_pin_select_ops(pin) == &dpllsim_input_pin_ops)
-		prop->capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		prop->capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE |
+				     DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE;
 	else
 		prop->capabilities = 0;
 }
@@ -191,6 +224,13 @@ static bool dpllsim_pin_direction_valid(const struct dpllsim_pin *pin)
 	       pin->direction == DPLL_PIN_DIRECTION_INPUT;
 }
 
+/* Priority is meaningful for input pins only */
+static bool dpllsim_pin_prio_valid(const struct dpllsim_pin *pin)
+{
+	return pin->direction == DPLL_PIN_DIRECTION_INPUT ||
+	       pin->prio == DPLLSIM_DEFAULT_PIN_PRIO;
+}
+
 static void dpllsim_pin_reset(struct dpllsim_pin *pin)
 {
 	pin->ops = dpllsim_pin_select_ops(pin);
@@ -205,7 +245,8 @@ static int dpllsim_order_pins(struct dpllsim_dev *sim)
 
 	list_for_each_entry(pin, &sim->pin_list, list) {
 		if (!dpllsim_pin_frequency_valid(pin) ||
-		    !dpllsim_pin_direction_valid(pin))
+		    !dpllsim_pin_direction_valid(pin) ||
+		    !dpllsim_pin_prio_valid(pin))
 			return -EINVAL;
 		count++;
 	}
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
index 73b846b66f79c7..f9fced337dc881 100644
--- a/drivers/dpll/dpllsim/dpllsim.h
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -12,6 +12,9 @@
 #include <linux/list.h>
 #include <linux/mutex.h>
 
+#define DPLLSIM_DEFAULT_PIN_PRIO		128
+#define DPLLSIM_PIN_PRIO_MAX			255
+
 #define DPLLSIM_MAX_PINS			128
 
 struct dpllsim_dev;
@@ -21,10 +24,15 @@ struct dpllsim_pin {
 	struct list_head list;
 	struct dpllsim_dev *dev;
 
-	/* Configuration, writable only until dpllsim_dev_frozen() */
+	/*
+	 * Configuration, set through configfs only until
+	 * dpllsim_dev_frozen(). The netlink prio setter changes prio at
+	 * runtime under dev->lock.
+	 */
 	enum dpll_pin_type type;
 	enum dpll_pin_direction direction;
 	u64 frequency;
+	u32 prio;
 
 	/* Resolved at live=1 */
 	int idx;
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 08/14] selftests: dpll: add pin priority test
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (6 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 07/14] dpll: dpllsim: Add support to get/set priority on input pins Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:50   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 09/14] dpll: dpllsim: Add support to set frequency on pins Petr Oros
                   ` (6 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Add a test that sets a new priority on an input pin, reads it back and
restores the original value. The range of priorities a device accepts is
not advertised over netlink, so the test tries one below the current
value and then the small values every driver accepts, since a large
current value may mark an unused pin outside the settable range, as the
ice driver does with 255. A driver may still refuse all of them for a
constraint the interface does not tell, so that is a skip on a device
provided by the user, with the reason the tool reported, and a failure
on the device the script created, whose simulator accepts every
priority. The test skips when the dpll tool lacks the prio keyword of
pin set, and the restore is deferred right after the original value is
read so that an interrupted run restores it too. A pin that does not
advertise the priority-can-change capability or does not report a
priority is a skip on a device provided by the user and a failure on the
device the script created, whose pins have both.

The dpllsim_add_pin helper gains an optional priority argument, which is
written only when it is given, so that a pin without one keeps the
default of the simulator. A priority is accepted for input pins only,
so the callers pass one for those alone.

The standard device lays its inputs out as gnss0 with priority 0 and
ext0 with priority 1, and the stress devices get random priorities on
their input pins.

Signed-off-by: Petr Oros <poros@redhat.com>
---
 .../drivers/net/dpll/dpll_features.sh         | 65 +++++++++++++++++++
 .../selftests/drivers/net/dpll/dpll_stress.sh | 15 +++--
 .../selftests/drivers/net/lib/sh/dpll_lib.sh  | 32 +++++++--
 3 files changed, 100 insertions(+), 12 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_features.sh b/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
index c73417a058e0a0..61003ba280d658 100755
--- a/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
@@ -16,11 +16,76 @@ source "$SCRIPTDIR/../lib/sh/dpll_lib.sh"
 
 ALL_TESTS="
 	test_pin_state_transitions
+	test_pin_priority_get_set
 "
 
 ##############################################################################
 # Pin-level feature tests
 
+test_pin_priority_get_set()
+{
+	local before target after reason candidate
+
+	RET=0
+
+	if [ -z "$PIN_TUNE_ID" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin priority set/get" \
+		    "no spare input pin"
+		return
+	fi
+	if ! dpll_pin_set_supports prio; then
+		log_test_skip "DPLL pin priority set/get" \
+		    "dpll tool lacks pin set prio"
+		return
+	fi
+	if ! dpll_pin_has_capability "$PIN_TUNE_ID" priority-can-change; then
+		dpll_log_test_skip_or_fail "DPLL pin priority set/get" \
+		    "priority cannot change"
+		return
+	fi
+
+	before=$(dpll_pin_get_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+	if [ -z "$before" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin priority set/get" \
+		    "prio not reported"
+		return
+	fi
+	defer dpll_pin_set_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"
+
+	# The accepted range is not advertised, so try one below the current
+	# value and then small values, a refusal of all of them is a skip on
+	# a device the user provided, the simulator accepts every priority.
+	target=""
+	for candidate in $((before - 1)) 1 7; do
+		[ "$candidate" -ge 0 ] && [ "$candidate" != "$before" ] ||
+			continue
+		if dpll_pin_set_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID" \
+				     "$candidate"; then
+			target=$candidate
+			break
+		fi
+		reason=${DPLL_LAST_ERR%%$'\n'*}
+	done
+	if [ -z "$target" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin priority set/get" \
+		    "cannot set a new prio${reason:+, $reason}"
+		return
+	fi
+	after=$(dpll_pin_get_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+	if [ "$after" != "$target" ]; then
+		check_err 1 "priority expected $target, got '$after'"
+	fi
+
+	dpll_pin_set_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"
+	check_err $? "cannot restore pin priority"
+	after=$(dpll_pin_get_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+	if [ "$after" != "$before" ]; then
+		check_err 1 "priority not restored, got '$after'"
+	fi
+
+	log_test "DPLL pin priority set/get"
+}
+
 test_pin_state_transitions()
 {
 	local before state after reason
diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
index ee543e2cd8703c..60e42107c67efe 100755
--- a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
@@ -77,13 +77,13 @@ create_stress_device()
 	local clock_id=$1
 	local num_pins=$2
 	local name="st$clock_id"
-	local i type freq dir
+	local i type freq dir prio
 	local dirs=(input output)
 
 	dpllsim_create_device "$name" "$clock_id" || return 1
 
 	# Always have at least one plain input pin, then random ones
-	if ! dpllsim_add_pin "$name" ext0 ext 10000000 input; then
+	if ! dpllsim_add_pin "$name" ext0 ext 10000000 input 1; then
 		dpllsim_delete_device "$name"
 		return 1
 	fi
@@ -94,8 +94,11 @@ create_stress_device()
 		# output one
 		dir=${dirs[RANDOM % 2]}
 		[ "$type" = mux ] && dir=input
-		if ! dpllsim_add_pin "$name" "pin$i" "$type" "$freq" \
-				     "$dir"; then
+		# Only input pins accept a priority
+		prio=""
+		[ "$dir" = input ] && prio=$((RANDOM % 128))
+		if ! dpllsim_add_pin "$name" "pin$i" "$type" "$freq" "$dir" \
+				     "$prio"; then
 			dpllsim_delete_device "$name"
 			return 1
 		fi
@@ -205,8 +208,8 @@ test_deploy_cycle()
 	fi
 	track_device "$name"
 
-	if ! dpllsim_add_pin "$name" gnss0 gnss 1 input ||
-	   ! dpllsim_add_pin "$name" ext0 ext 10000000 input ||
+	if ! dpllsim_add_pin "$name" gnss0 gnss 1 input 0 ||
+	   ! dpllsim_add_pin "$name" ext0 ext 10000000 input 1 ||
 	   ! dpllsim_add_pin "$name" out0 ext 10000000 output; then
 		check_err 1 "cannot add pins"
 		log_test "DPLL deploy/undeploy cycling"
diff --git a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
index ee91ad1722971d..1c3e27eb09775d 100644
--- a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
@@ -376,6 +376,11 @@ dpll_pin_has_capability()
 	   <<< "$out" &> /dev/null
 }
 
+dpll_pin_get_prio()
+{
+	__dpll_pin_dev_field "$1" "$2" '.prio'
+}
+
 ##############################################################################
 # Pin setters
 #
@@ -397,6 +402,16 @@ dpll_pin_set_state()
 			     state "$state" 2>&1 >/dev/null)
 }
 
+dpll_pin_set_prio()
+{
+	local dev_id=$1
+	local pin_id=$2
+	local prio=$3
+
+	DPLL_LAST_ERR=$(dpll pin set id "$pin_id" parent-device "$dev_id" \
+			     prio "$prio" 2>&1 >/dev/null)
+}
+
 ##############################################################################
 # dpllsim configfs interface
 #
@@ -478,7 +493,9 @@ dpllsim_create_device()
 
 # $1: device name, $2: pin name (becomes the board-label), $3: type (gnss,
 # int-oscillator, ext, synce-eth-port, mux), $4: frequency in Hz,
-# $5: direction (input/output)
+# $5: direction (input/output), $6: prio (optional, input pins only).
+# The priority is written only when one is given, so a pin without one
+# keeps the default of the simulator.
 dpllsim_add_pin()
 {
 	local dev=$1
@@ -486,13 +503,16 @@ dpllsim_add_pin()
 	local type=$3
 	local frequency=$4
 	local direction=$5
+	local prio=${6:-}
 	local pin_path="$DPLLSIM_CFG/$dev/pins/$pin"
 
 	mkdir "$pin_path" 2>/dev/null || return 1
 
 	if ! echo "$type" 2>/dev/null > "$pin_path/type" ||
 	   ! echo "$frequency" 2>/dev/null > "$pin_path/frequency" ||
-	   ! echo "$direction" 2>/dev/null > "$pin_path/direction"; then
+	   ! echo "$direction" 2>/dev/null > "$pin_path/direction" ||
+	   { [ -n "$prio" ] &&
+	     ! echo "$prio" 2>/dev/null > "$pin_path/prio"; }; then
 		rmdir "$pin_path" 2>/dev/null
 		return 1
 	fi
@@ -540,8 +560,8 @@ dpllsim_alloc_clock_id()
 # Create a device with the standard 3-pin layout, deploy it and resolve
 # the netlink device id. Sets DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and
 # DPLLSIM_DEV_ID.
-#   gnss0: GNSS, 1 Hz, input
-#   ext0: EXT, 10 MHz, input
+#   gnss0: GNSS, 1 Hz, input, prio 0 (active reference)
+#   ext0: EXT, 10 MHz, input, prio 1
 #   out0: EXT, 10 MHz, output
 # shellcheck disable=SC2120 # the argument is optional
 dpllsim_setup_standard_device()
@@ -553,8 +573,8 @@ dpllsim_setup_standard_device()
 	name="std$clock_id"
 	dpllsim_create_device "$name" "$clock_id" || return 1
 
-	if ! dpllsim_add_pin "$name" gnss0 gnss 1 input ||
-	   ! dpllsim_add_pin "$name" ext0 ext 10000000 input ||
+	if ! dpllsim_add_pin "$name" gnss0 gnss 1 input 0 ||
+	   ! dpllsim_add_pin "$name" ext0 ext 10000000 input 1 ||
 	   ! dpllsim_add_pin "$name" out0 ext 10000000 output; then
 		dpllsim_delete_device "$name"
 		return 1
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 09/14] dpll: dpllsim: Add support to set frequency on pins
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (7 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 08/14] selftests: dpll: add pin priority test Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:50   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 10/14] selftests: dpll: add pin frequency test Petr Oros
                   ` (5 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Add support to set the frequency of input and output pins. The DPLL
core validates the requested value against the supported frequency
list the pin advertises, so the frequency_set callback only has to
store it. The same per type tables that validate the configured
frequency at live time serve as the supported list. The configfs
attribute is rejected with EBUSY while the device is live, so only the
netlink setter changes the frequency of a registered pin and the two
cannot race.

Signed-off-by: Petr Oros <poros@redhat.com>
---
 drivers/dpll/dpllsim/dpll.c    | 15 +++++++++++++++
 drivers/dpll/dpllsim/dpllsim.h |  4 ++--
 2 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index 8a302c2537c880..3caccd38a7e74a 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -79,6 +79,19 @@ static int dpllsim_pin_frequency_get(const struct dpll_pin *pin,
 	return 0;
 }
 
+static int dpllsim_pin_frequency_set(const struct dpll_pin *pin,
+				     void *pin_priv,
+				     const struct dpll_device *dpll,
+				     void *dpll_priv, u64 frequency,
+				     struct netlink_ext_ack *extack)
+{
+	struct dpllsim_pin *sim_pin = pin_priv;
+
+	guard(mutex)(&sim_pin->dev->lock);
+	sim_pin->frequency = frequency;
+	return 0;
+}
+
 static int dpllsim_pin_direction_get(const struct dpll_pin *pin,
 				     void *pin_priv,
 				     const struct dpll_device *dpll,
@@ -148,6 +161,7 @@ static int dpllsim_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
 
 static const struct dpll_pin_ops dpllsim_input_pin_ops = {
 	.frequency_get = dpllsim_pin_frequency_get,
+	.frequency_set = dpllsim_pin_frequency_set,
 	.direction_get = dpllsim_pin_direction_get,
 	.state_on_dpll_get = dpllsim_pin_state_get,
 	.state_on_dpll_set = dpllsim_pin_state_set,
@@ -157,6 +171,7 @@ static const struct dpll_pin_ops dpllsim_input_pin_ops = {
 
 static const struct dpll_pin_ops dpllsim_output_pin_ops = {
 	.frequency_get = dpllsim_pin_frequency_get,
+	.frequency_set = dpllsim_pin_frequency_set,
 	.direction_get = dpllsim_pin_direction_get,
 	.state_on_dpll_get = dpllsim_pin_state_get,
 };
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
index f9fced337dc881..f59447d26a8668 100644
--- a/drivers/dpll/dpllsim/dpllsim.h
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -26,8 +26,8 @@ struct dpllsim_pin {
 
 	/*
 	 * Configuration, set through configfs only until
-	 * dpllsim_dev_frozen(). The netlink prio setter changes prio at
-	 * runtime under dev->lock.
+	 * dpllsim_dev_frozen(). The netlink setters change prio and
+	 * frequency at runtime under dev->lock.
 	 */
 	enum dpll_pin_type type;
 	enum dpll_pin_direction direction;
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 10/14] selftests: dpll: add pin frequency test
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (8 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 09/14] dpll: dpllsim: Add support to set frequency on pins Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:51   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 11/14] dpll: dpllsim: Add support to adjust phase Petr Oros
                   ` (4 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Add a test that picks a supported frequency other than the current one
from the frequency-supported list of an input pin, sets it, reads it
back and restores the original value. Both ends of every advertised
range are candidates, so that a pin advertising a single range wider
than one value still has one. It skips when the dpll tool lacks the
frequency keyword of pin set, and the restore is deferred right after
the original value is read so that an interrupted run restores it too.
A pin that reports no frequency or advertises a single frequency only
is a skip on a device provided by the user and a failure on the device
the script created, whose pins advertise several.

The driver may refuse the candidate. A driver without a frequency
setter refuses every value with EOPNOTSUPP, and a driver with
constraints of its own refuses values the advertised ranges allow,
zl3073x for instance wants the frequencies of an output pair to divide
each other. On a device provided by the user a refused set is a skip
that names the reason the tool reported, which the setter keeps in
DPLL_LAST_ERR, on the created device the simulator accepts every
advertised value and the refusal is a failure. Once the set went
through, a refused restore is a failure on either.

Signed-off-by: Petr Oros <poros@redhat.com>
---
 .../drivers/net/dpll/dpll_features.sh         | 64 +++++++++++++++++++
 .../selftests/drivers/net/lib/sh/dpll_lib.sh  |  5 ++
 2 files changed, 69 insertions(+)

diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_features.sh b/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
index 61003ba280d658..a39098fbf73135 100755
--- a/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
@@ -17,6 +17,7 @@ source "$SCRIPTDIR/../lib/sh/dpll_lib.sh"
 ALL_TESTS="
 	test_pin_state_transitions
 	test_pin_priority_get_set
+	test_pin_frequency_set_get
 "
 
 ##############################################################################
@@ -165,6 +166,69 @@ test_pin_state_transitions()
 	log_test "DPLL pin state transitions"
 }
 
+test_pin_frequency_set_get()
+{
+	local before target after reason
+
+	RET=0
+
+	if [ -z "$PIN_TUNE_ID" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
+		    "no spare input pin"
+		return
+	fi
+	if ! dpll_pin_set_supports frequency; then
+		log_test_skip "DPLL pin frequency set/get" \
+		    "dpll tool lacks pin set frequency"
+		return
+	fi
+
+	before=$(dpll_pin_get_frequency "$PIN_TUNE_ID")
+	if [ -z "$before" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
+		    "frequency not reported"
+		return
+	fi
+	defer dpll_pin_set_frequency "$PIN_TUNE_ID" "$before"
+
+	# Pick a supported frequency different from the current one. Both
+	# ends of every supported range are candidates, so a pin with a
+	# single range still has an alternative unless it is one value wide.
+	target=$(dpll_pin_json "$PIN_TUNE_ID" | \
+		 jq -r --argjson cur "$before" \
+		    '[.["frequency-supported"][]? |
+		      .["frequency-min"], .["frequency-max"] |
+		      numbers | select(. != $cur)][0] // empty')
+	if [ -z "$target" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
+		    "no alternative supported frequency"
+		return
+	fi
+
+	# A driver without a frequency setter or with constraints of its own
+	# may refuse the candidate, a skip on a device the user provided, the
+	# simulator accepts every advertised value.
+	if ! dpll_pin_set_frequency "$PIN_TUNE_ID" "$target"; then
+		reason=${DPLL_LAST_ERR%%$'\n'*}
+		dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
+		    "cannot set frequency=$target${reason:+, $reason}"
+		return
+	fi
+	after=$(dpll_pin_get_frequency "$PIN_TUNE_ID")
+	if [ "$after" != "$target" ]; then
+		check_err 1 "frequency expected $target, got '$after'"
+	fi
+
+	dpll_pin_set_frequency "$PIN_TUNE_ID" "$before"
+	check_err $? "cannot restore frequency"
+	after=$(dpll_pin_get_frequency "$PIN_TUNE_ID")
+	if [ "$after" != "$before" ]; then
+		check_err 1 "frequency not restored, got '$after'"
+	fi
+
+	log_test "DPLL pin frequency set/get"
+}
+
 ##############################################################################
 
 cleanup()
diff --git a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
index 1c3e27eb09775d..9718d73c60b444 100644
--- a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
@@ -412,6 +412,11 @@ dpll_pin_set_prio()
 			     prio "$prio" 2>&1 >/dev/null)
 }
 
+dpll_pin_set_frequency()
+{
+	DPLL_LAST_ERR=$(dpll pin set id "$1" frequency "$2" 2>&1 >/dev/null)
+}
+
 ##############################################################################
 # dpllsim configfs interface
 #
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 11/14] dpll: dpllsim: Add support to adjust phase
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (9 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 10/14] selftests: dpll: add pin frequency test Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:51   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 12/14] selftests: dpll: add pin phase adjust test Petr Oros
                   ` (3 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Add support to get and set the phase adjustment of input and output
pins. The pins advertise a granularity of 100 ps and a symmetric range
whose ends are the largest multiples of the granularity that fit a
32-bit signed value, so that the advertised ends themselves are
settable, the core validates the requested value against both and the
driver stores it. The adjustment is reset to zero every time the device
goes live, so that repeated deploy cycles start from a known state. It
becomes the baseline of the simulated phase offset measurements once
those exist.

Signed-off-by: Petr Oros <poros@redhat.com>
---
 drivers/dpll/dpllsim/dpll.c    | 40 ++++++++++++++++++++++++++++++++++
 drivers/dpll/dpllsim/dpllsim.h |  1 +
 2 files changed, 41 insertions(+)

diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index 3caccd38a7e74a..77e7ae994bf104 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -12,6 +12,11 @@
 
 #include "dpllsim.h"
 
+#define DPLLSIM_PHASE_ADJUST_GRAN		100	/* ps */
+/* Largest multiple of the granularity, so the range ends are settable */
+#define DPLLSIM_PHASE_ADJUST_MAX \
+	(S32_MAX - S32_MAX % DPLLSIM_PHASE_ADJUST_GRAN)
+
 static struct dpll_pin_frequency dpllsim_freq_1pps[] = {
 	DPLL_PIN_FREQUENCY_RANGE(1, 1),
 };
@@ -159,6 +164,33 @@ static int dpllsim_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
 	return 0;
 }
 
+static int dpllsim_pin_phase_adjust_get(const struct dpll_pin *pin,
+					void *pin_priv,
+					const struct dpll_device *dpll,
+					void *dpll_priv, s32 *phase_adjust,
+					struct netlink_ext_ack *extack)
+{
+	struct dpllsim_pin *sim_pin = pin_priv;
+
+	guard(mutex)(&sim_pin->dev->lock);
+	*phase_adjust = sim_pin->phase_adjust;
+	return 0;
+}
+
+static int dpllsim_pin_phase_adjust_set(const struct dpll_pin *pin,
+					void *pin_priv,
+					const struct dpll_device *dpll,
+					void *dpll_priv,
+					const s32 phase_adjust,
+					struct netlink_ext_ack *extack)
+{
+	struct dpllsim_pin *sim_pin = pin_priv;
+
+	guard(mutex)(&sim_pin->dev->lock);
+	sim_pin->phase_adjust = phase_adjust;
+	return 0;
+}
+
 static const struct dpll_pin_ops dpllsim_input_pin_ops = {
 	.frequency_get = dpllsim_pin_frequency_get,
 	.frequency_set = dpllsim_pin_frequency_set,
@@ -167,6 +199,8 @@ static const struct dpll_pin_ops dpllsim_input_pin_ops = {
 	.state_on_dpll_set = dpllsim_pin_state_set,
 	.prio_get = dpllsim_pin_prio_get,
 	.prio_set = dpllsim_pin_prio_set,
+	.phase_adjust_get = dpllsim_pin_phase_adjust_get,
+	.phase_adjust_set = dpllsim_pin_phase_adjust_set,
 };
 
 static const struct dpll_pin_ops dpllsim_output_pin_ops = {
@@ -174,6 +208,8 @@ static const struct dpll_pin_ops dpllsim_output_pin_ops = {
 	.frequency_set = dpllsim_pin_frequency_set,
 	.direction_get = dpllsim_pin_direction_get,
 	.state_on_dpll_get = dpllsim_pin_state_get,
+	.phase_adjust_get = dpllsim_pin_phase_adjust_get,
+	.phase_adjust_set = dpllsim_pin_phase_adjust_set,
 };
 
 static const struct dpll_pin_ops *
@@ -189,6 +225,9 @@ static void dpllsim_pin_props(struct dpllsim_pin *pin,
 {
 	prop->type = pin->type;
 	prop->board_label = config_item_name(&pin->item);
+	prop->phase_range.min = -DPLLSIM_PHASE_ADJUST_MAX;
+	prop->phase_range.max = DPLLSIM_PHASE_ADJUST_MAX;
+	prop->phase_gran = DPLLSIM_PHASE_ADJUST_GRAN;
 
 	switch (pin->type) {
 	case DPLL_PIN_TYPE_GNSS:
@@ -250,6 +289,7 @@ static void dpllsim_pin_reset(struct dpllsim_pin *pin)
 {
 	pin->ops = dpllsim_pin_select_ops(pin);
 	pin->state = DPLL_PIN_STATE_DISCONNECTED;
+	pin->phase_adjust = 0;
 }
 
 /* Build sim->pins in registration order, the order the pins were created */
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
index f59447d26a8668..886ac27f5f92d6 100644
--- a/drivers/dpll/dpllsim/dpllsim.h
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -43,6 +43,7 @@ struct dpllsim_pin {
 
 	/* Simulated state, guarded by dev->lock */
 	enum dpll_pin_state state;
+	s32 phase_adjust;
 };
 
 struct dpllsim_dev {
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 12/14] selftests: dpll: add pin phase adjust test
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (10 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 11/14] dpll: dpllsim: Add support to adjust phase Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:51   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 13/14] selftests: dpll: add concurrency stress tests Petr Oros
                   ` (2 subsequent siblings)
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Add a test that reads the phase adjustment range and granularity a
pin advertises, picks the largest multiple of the granularity inside
the range, or the smallest one when that is already the current value,
sets it, reads it back and restores the original value. It skips when
the dpll tool lacks the phase-adjust keyword of pin set and when the
range is too narrow to pick a different value. A pin that reports no
phase adjustment or no range is a skip on a device provided by the user
and a failure on the device the script created, whose pins report
both.

The core refuses a value that is not a multiple of the granularity a
driver reports, the current value included, so a pin that sits at such
a value could not be put back. The test bails out on that before it
schedules the restore, which is deferred right after so that an
interrupted run restores the value too.

The granularity is only reported by drivers that have the core enforce
one. Without it the target is computed with a granularity of 1, and a
driver may refuse it for a granularity of its own or for any other
constraint the interface does not advertise. A refused set is therefore
a skip on a device provided by the user, with the reason the tool
reported, and a failure on the device the script created, whose
simulator accepts every value its range and granularity allow. The
test ends there, a value that was not set has nothing to read back.

Signed-off-by: Petr Oros <poros@redhat.com>
---
 .../drivers/net/dpll/dpll_features.sh         | 85 +++++++++++++++++++
 .../selftests/drivers/net/lib/sh/dpll_lib.sh  | 10 +++
 2 files changed, 95 insertions(+)

diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_features.sh b/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
index a39098fbf73135..1466ddd6538348 100755
--- a/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_features.sh
@@ -18,6 +18,7 @@ ALL_TESTS="
 	test_pin_state_transitions
 	test_pin_priority_get_set
 	test_pin_frequency_set_get
+	test_pin_phase_adjust_set_get
 "
 
 ##############################################################################
@@ -166,6 +167,90 @@ test_pin_state_transitions()
 	log_test "DPLL pin state transitions"
 }
 
+test_pin_phase_adjust_set_get()
+{
+	local before target after json min max gran reason
+
+	RET=0
+
+	if [ -z "$PIN_TUNE_ID" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+		    "no spare input pin"
+		return
+	fi
+	if ! dpll_pin_set_supports phase-adjust; then
+		log_test_skip "DPLL pin phase-adjust set/get" \
+		    "dpll tool lacks pin set phase-adjust"
+		return
+	fi
+
+	before=$(dpll_pin_get_phase_adjust "$PIN_TUNE_ID")
+	if [ -z "$before" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+		    "phase adjust not reported"
+		return
+	fi
+
+	# Pick a target inside the advertised range different from the
+	# current value, a multiple of the granularity, which is taken as 1
+	# when the driver reports none.
+	json=$(dpll_pin_json "$PIN_TUNE_ID")
+	min=$(jq -r '.["phase-adjust-min"] // empty' <<< "$json")
+	max=$(jq -r '.["phase-adjust-max"] // empty' <<< "$json")
+	gran=$(jq -r '.["phase-adjust-gran"] // empty' <<< "$json")
+	if [ -z "$min" ] || [ -z "$max" ]; then
+		dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+		    "no adjust range"
+		return
+	fi
+	# A current value that is not a granularity multiple could not be
+	# written back, so bail out before the restore is scheduled.
+	if [ -n "$gran" ] && [ $((before % gran)) -ne 0 ]; then
+		dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+		    "current phase adjust not a granularity multiple"
+		return
+	fi
+	[ -n "$gran" ] || gran=1
+	defer dpll_pin_set_phase_adjust "$PIN_TUNE_ID" "$before"
+
+	# Shell division truncates towards zero, so round the bounds towards
+	# the inside of the range explicitly, which matters for negative ones.
+	target=$((max / gran * gran))
+	[ "$target" -gt "$max" ] && target=$((target - gran))
+	if [ "$target" = "$before" ]; then
+		target=$((min / gran * gran))
+		[ "$target" -lt "$min" ] && target=$((target + gran))
+	fi
+	if [ "$target" = "$before" ] || [ "$target" -gt "$max" ] ||
+	   [ "$target" -lt "$min" ]; then
+		log_test_skip "DPLL pin phase-adjust set/get" "range too narrow"
+		return
+	fi
+
+	# A driver may enforce a granularity it does not report or another
+	# constraint, a skip on a device the user provided, the simulator
+	# accepts every value its range and granularity allow.
+	if ! dpll_pin_set_phase_adjust "$PIN_TUNE_ID" "$target"; then
+		reason=${DPLL_LAST_ERR%%$'\n'*}
+		dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+		    "cannot set phase-adjust=$target${reason:+, $reason}"
+		return
+	fi
+	after=$(dpll_pin_get_phase_adjust "$PIN_TUNE_ID")
+	if [ "$after" != "$target" ]; then
+		check_err 1 "phase-adjust expected $target, got '$after'"
+	fi
+
+	dpll_pin_set_phase_adjust "$PIN_TUNE_ID" "$before"
+	check_err $? "cannot restore phase-adjust"
+	after=$(dpll_pin_get_phase_adjust "$PIN_TUNE_ID")
+	if [ "$after" != "$before" ]; then
+		check_err 1 "phase-adjust not restored, got '$after'"
+	fi
+
+	log_test "DPLL pin phase-adjust set/get"
+}
+
 test_pin_frequency_set_get()
 {
 	local before target after reason
diff --git a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
index 9718d73c60b444..8fa2a9936a0eb2 100644
--- a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
@@ -381,6 +381,11 @@ dpll_pin_get_prio()
 	__dpll_pin_dev_field "$1" "$2" '.prio'
 }
 
+dpll_pin_get_phase_adjust()
+{
+	__dpll_pin_field "$1" '.["phase-adjust"]'
+}
+
 ##############################################################################
 # Pin setters
 #
@@ -417,6 +422,11 @@ dpll_pin_set_frequency()
 	DPLL_LAST_ERR=$(dpll pin set id "$1" frequency "$2" 2>&1 >/dev/null)
 }
 
+dpll_pin_set_phase_adjust()
+{
+	DPLL_LAST_ERR=$(dpll pin set id "$1" phase-adjust "$2" 2>&1 >/dev/null)
+}
+
 ##############################################################################
 # dpllsim configfs interface
 #
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 13/14] selftests: dpll: add concurrency stress tests
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (11 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 12/14] selftests: dpll: add pin phase adjust test Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:52   ` Ivan Vecera
  2026-11-05 15:18 ` [PATCH net-next v2 14/14] selftests: dpll: add reference leak tests Petr Oros
  2026-11-06 12:27 ` [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Jiri Pirko
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

Extend the stress script with tests that register and unregister
devices from several processes at once and that keep a number of
devices with many pins live at the same time, checking that every one
of them is visible on netlink.

The workers run in subshells, so the parent cannot track the device a
worker has in flight. Each worker removes it from its own exit trap
instead, and the parent reserves a range of clock IDs for every worker,
so that the devices of a worker cannot collide with those of another
worker or of the parent and whatever a killed worker left behind can
be removed by name, at the end of the test and from the cleanup
handler. The handler stops the recorded workers together with the jobs
of the shell, gives them a few seconds to run their exit traps before
forcing the survivors, and goes on without a worker that does not die,
since one stuck in the kernel would hang the teardown. Deleting a
device treats a part of it that is gone by the time its step runs as
removed, so that a removal running concurrently with the one of a
worker does not turn into a spurious failure. The workers inherit the
open lock file of the library, so the lock is held until the script
and every worker have exited.

Signed-off-by: Petr Oros <poros@redhat.com>
---
v2:
- quote the failed and ret expansions (netdev CI shellcheck)

v1: https://lore.kernel.org/all/20261103093910.596225-14-poros@redhat.com/
---
 .../selftests/drivers/net/dpll/dpll_stress.sh | 219 +++++++++++++++++-
 .../selftests/drivers/net/lib/sh/dpll_lib.sh  |  30 ++-
 2 files changed, 241 insertions(+), 8 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
index 60e42107c67efe..2a6813871f34ea 100755
--- a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
@@ -15,13 +15,24 @@ source "$SCRIPTDIR/../lib/sh/dpll_lib.sh"
 
 ALL_TESTS="
 	test_rapid_create_delete
+	test_concurrent_workers
 	test_pin_state_flooding
+	test_max_devices
 	test_deploy_cycle
 "
 
 STRESS_ITERATIONS=${STRESS_ITERATIONS:-20}
+STRESS_DEVICES=${STRESS_DEVICES:-5}
+# The simulator accepts up to 128 pins per device (DPLLSIM_MAX_PINS)
+STRESS_PINS_PER_DEVICE=${STRESS_PINS_PER_DEVICE:-10}
+STRESS_CONCURRENT_PROCS=${STRESS_CONCURRENT_PROCS:-4}
+STRESS_WORKER_ITERATIONS=${STRESS_WORKER_ITERATIONS:-10}
 
 CREATED_DEVICES=()
+BACKGROUND_PIDS=()
+# Base clock-ids of the workers in flight, their devices are named
+# st<base + iteration>
+WORKER_BASES=()
 
 track_device()
 {
@@ -39,18 +50,103 @@ untrack_device()
 	CREATED_DEVICES=("${remaining[@]}")
 }
 
+# Remove whatever a worker started from the given base clock-id left
+# behind, which is the device in flight when the worker had to be killed.
+# A device that does not exist counts as deleted.
+delete_worker_devices()
+{
+	local base=$1
+	local i name
+
+	for i in $(seq 1 "$STRESS_WORKER_ITERATIONS"); do
+		name="st$((base + i))"
+		dpllsim_delete_device "$name" ||
+			log_info "cannot delete device $name"
+	done
+}
+
+# Return 0 while the process still runs. A process that has exited but
+# has not been reaped yet is a zombie, state Z in /proc/<pid>/stat, and
+# counts as gone, or a wait on it would spin until the forced kill.
+pid_running()
+{
+	local stat
+
+	kill -0 "$1" 2>/dev/null || return 1
+	# The state follows the parenthesized command name, which may
+	# contain spaces and parentheses. The ".*" is greedy, so the
+	# expression strips everything up to the last ") " of the line, and
+	# a ")" inside the name is harmless.
+	stat=$(sed 's/^.*) //' "/proc/$1/stat" 2>/dev/null) || return 1
+	[ "${stat%% *}" != "Z" ]
+}
+
+# Return 0 once none of the given processes runs any more, 1 when some
+# still does after about five seconds.
+pids_gone()
+{
+	local i pid alive
+
+	for i in $(seq 1 50); do
+		alive=0
+		for pid in "$@"; do
+			pid_running "$pid" && alive=1
+		done
+		[ "$alive" -eq 0 ] && return 0
+		sleep 0.1
+	done
+	return 1
+}
+
 cleanup()
 {
-	local dev
+	local pid dev base
+	local pids=() shell_jobs=()
 
 	# A second signal must not abort the teardown half way.
 	trap '' INT TERM
 
+	# A worker forked right before the signal arrived may not be
+	# recorded yet, so the jobs of the shell are taken as well. The
+	# jobs builtin reports them from a subshell only when it is the
+	# whole command of that subshell.
+	mapfile -t shell_jobs < <(jobs -p)
+	mapfile -t pids < <(printf '%s\n' "${BACKGROUND_PIDS[@]}" \
+				   "${shell_jobs[@]}" | grep . | sort -u)
+	for pid in "${pids[@]}"; do
+		kill "$pid" 2>/dev/null
+	done
+	# Give the workers a few seconds to run their exit traps, then
+	# force the survivors and give those a few seconds to die. The
+	# kernel cannot recycle the pid of a child until it is reaped,
+	# which the wait below does, so the forced kill cannot hit a
+	# process that reused one. A worker stuck in the kernel does not
+	# die of the forced kill before whatever it waits for returns, so
+	# only the workers that are gone are waited for, which reaps them,
+	# and the teardown goes on without the rest. The removal of what
+	# such a worker left behind can then fail or race with it, which
+	# is logged like any other failed removal.
+	pids_gone "${pids[@]}"
+	for pid in "${pids[@]}"; do
+		kill -KILL "$pid" 2>/dev/null
+	done
+	pids_gone "${pids[@]}"
+	for pid in "${pids[@]}"; do
+		if pid_running "$pid"; then
+			log_info "worker $pid did not exit, giving up"
+		else
+			wait "$pid" 2>/dev/null
+		fi
+	done
+
 	defer_scopes_cleanup
 	for dev in "${CREATED_DEVICES[@]}"; do
 		dpllsim_delete_device "$dev" ||
 			log_info "cannot delete device $dev"
 	done
+	for base in "${WORKER_BASES[@]}"; do
+		delete_worker_devices "$base"
+	done
 	# The device whose creation a signal interrupted is recorded by the
 	# library alone, a double delete of a tracked one is harmless.
 	if [ -n "$DPLLSIM_CREATED_NAME" ]; then
@@ -138,6 +234,83 @@ test_rapid_create_delete()
 	log_test "DPLL rapid create/delete ($STRESS_ITERATIONS iterations)"
 }
 
+# The exit trap of a worker. The worker runs in a subshell, so
+# track_device() cannot reach CREATED_DEVICES of the parent. Remove the
+# device in flight from the exit trap instead, or killing the worker
+# leaks it. The trap saves the exit status first, so that the removal
+# cannot mask a failed iteration, and drops the traps, since bash runs
+# the EXIT trap again when a signal handler exits.
+worker_exit()
+{
+	local status=$?
+
+	trap - EXIT TERM INT
+	if [ -n "$DPLLSIM_CREATED_NAME" ]; then
+		dpllsim_delete_device "$DPLLSIM_CREATED_NAME"
+		DPLLSIM_CREATED_NAME=""
+	fi
+	exit "$status"
+}
+
+# Create and delete STRESS_WORKER_ITERATIONS devices with clock-ids
+# following the given base, in a subshell. Each worker derives its own
+# unique clock-ids from a range reserved for it, so its devices can never
+# be confused with those of another worker or of the parent. The parent
+# records the base of every worker, so that the device a killed worker
+# left behind can be removed by name.
+concurrent_worker()
+{
+	# The workers inherit the state of the generator from the parent
+	# and would draw the same pin layouts, so reseed it with the pid of
+	# the subshell first.
+	RANDOM=$BASHPID
+	local base=$1
+	local i name failed=0
+
+	trap worker_exit EXIT TERM INT
+
+	for i in $(seq 1 "$STRESS_WORKER_ITERATIONS"); do
+		if ! create_stress_device $((base + i)) 3; then
+			failed=$((failed + 1))
+			continue
+		fi
+		name=$DPLLSIM_CREATED_NAME
+		dpllsim_delete_device "$name" ||
+			failed=$((failed + 1))
+	done
+
+	[ "$failed" -eq 0 ]
+}
+
+test_concurrent_workers()
+{
+	local pid base failed=0
+
+	RET=0
+
+	for _ in $(seq 1 "$STRESS_CONCURRENT_PROCS"); do
+		dpllsim_alloc_clock_id_range $((STRESS_WORKER_ITERATIONS + 1))
+		base=$DPLLSIM_CLOCK_ID
+		WORKER_BASES+=("$base")
+		concurrent_worker "$base" &
+		BACKGROUND_PIDS+=($!)
+	done
+
+	for pid in "${BACKGROUND_PIDS[@]}"; do
+		wait "$pid" || failed=1
+	done
+	BACKGROUND_PIDS=()
+
+	check_err "$failed" "some concurrent workers failed"
+
+	for base in "${WORKER_BASES[@]}"; do
+		delete_worker_devices "$base"
+	done
+	WORKER_BASES=()
+
+	log_test "DPLL concurrent workers ($STRESS_CONCURRENT_PROCS processes)"
+}
+
 test_pin_state_flooding()
 {
 	local clock_id name dev_id pin_id i last state fails=0
@@ -193,6 +366,50 @@ test_pin_state_flooding()
 	log_test "DPLL pin state flooding"
 }
 
+test_max_devices()
+{
+	local i clock_id created=0 visible
+	local test_devs=() clock_ids=()
+
+	RET=0
+
+	for i in $(seq 1 "$STRESS_DEVICES"); do
+		dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
+		create_stress_device "$clock_id" \
+				     "$STRESS_PINS_PER_DEVICE" || break
+		track_device "$DPLLSIM_CREATED_NAME"
+		test_devs+=("$DPLLSIM_CREATED_NAME")
+		clock_ids+=("$clock_id")
+		created=$((created + 1))
+	done
+
+	if [ "$created" -lt "$STRESS_DEVICES" ]; then
+		check_err 1 "created only $created of $STRESS_DEVICES devices"
+	fi
+
+	# Every created device must be visible on netlink.
+	visible=0
+	for clock_id in "${clock_ids[@]}"; do
+		if dpll_device_wait_by_clock_id "$clock_id" dpllsim \
+		   > /dev/null; then
+			visible=$((visible + 1))
+		fi
+	done
+	if [ "$visible" -ne "$created" ]; then
+		check_err 1 "only $visible of $created devices visible"
+	fi
+
+	for i in "${test_devs[@]}"; do
+		if dpllsim_delete_device "$i"; then
+			untrack_device "$i"
+		else
+			check_err 1 "cannot delete device $i"
+		fi
+	done
+
+	log_test "DPLL maximum devices ($STRESS_DEVICES devices)"
+}
+
 test_deploy_cycle()
 {
 	local clock_id name i
diff --git a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
index 8fa2a9936a0eb2..ada10ee4394fd5 100644
--- a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
@@ -54,8 +54,10 @@ dpll_lib_check_prereqs()
 # The scripts that create dpllsim devices share the one dpllsim directory
 # and cannot run alongside each other, so each takes a lock on
 # DPLL_LOCK_FILE before it mounts or creates anything and skips when
-# another run holds it. The file lives under /run rather than in the
-# world-writable /tmp, where a planted symlink could redirect the open.
+# another run holds it. The workers a script forks inherit the open file
+# and keep the lock until they exit. The file lives under /run rather
+# than in the world-writable /tmp, where a planted symlink could
+# redirect the open.
 DPLL_RUN_DIR="/run/dpll-selftest"
 DPLL_LOCK_FILE="$DPLL_RUN_DIR/lock"
 DPLL_LOCK_FD=""
@@ -545,8 +547,12 @@ dpllsim_undeploy()
 
 # Take the device offline and remove it together with all its pins.
 # Every step is attempted even when an earlier one failed, the return
-# value is non-zero when any did. A device that does not exist counts as
-# deleted.
+# value is non-zero when any did. A device that does not exist counts
+# as deleted, and so does a part of it that is gone by the time its
+# step runs, the live file of the undeploy and the pin or device
+# directory of an rmdir, so that a removal running concurrently, or an
+# earlier one whose later steps failed, does not turn into a spurious
+# failure here.
 dpllsim_delete_device()
 {
 	local dev_path="$DPLLSIM_CFG/$1"
@@ -554,12 +560,12 @@ dpllsim_delete_device()
 
 	[ -d "$dev_path" ] || return 0
 
-	dpllsim_undeploy "$1" || ret=1
+	dpllsim_undeploy "$1" || [ ! -e "$dev_path/live" ] || ret=1
 	for pin_path in "$dev_path"/pins/*/; do
 		[ -d "$pin_path" ] || continue
-		rmdir "$pin_path" || ret=1
+		rmdir "$pin_path" || [ ! -d "$pin_path" ] || ret=1
 	done
-	rmdir "$dev_path" || ret=1
+	rmdir "$dev_path" || [ ! -d "$dev_path" ] || ret=1
 
 	return "$ret"
 }
@@ -572,6 +578,16 @@ dpllsim_alloc_clock_id()
 	DPLLSIM_CLOCK_ID=$((DPLLSIM_NEXT_CLOCK_ID++))
 }
 
+# Set DPLLSIM_CLOCK_ID to the first of $1 consecutive clock-ids unique
+# within this test run and move the allocator past them, for a worker
+# that derives the clock-ids of its devices from it. A device the parent
+# creates afterwards cannot collide with one of the worker.
+dpllsim_alloc_clock_id_range()
+{
+	DPLLSIM_CLOCK_ID=$DPLLSIM_NEXT_CLOCK_ID
+	DPLLSIM_NEXT_CLOCK_ID=$((DPLLSIM_NEXT_CLOCK_ID + $1))
+}
+
 # Create a device with the standard 3-pin layout, deploy it and resolve
 # the netlink device id. Sets DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and
 # DPLLSIM_DEV_ID.
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH net-next v2 14/14] selftests: dpll: add reference leak tests
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (12 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 13/14] selftests: dpll: add concurrency stress tests Petr Oros
@ 2026-11-05 15:18 ` Petr Oros
  2026-11-06 10:47   ` Ivan Vecera
  2026-11-06 12:27 ` [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Jiri Pirko
  14 siblings, 1 reply; 30+ messages in thread
From: Petr Oros @ 2026-11-05 15:18 UTC (permalink / raw)
  To: netdev
  Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest

With CONFIG_DPLL_REFCNT_TRACKER the DPLL core keeps the stack traces of
unreleased device and pin references in debugfs. The entries do not
name their owner, so add library helpers that snapshot the entries
present before a test creates its devices and fail when an entry that
is not in the snapshot still holds references afterwards. An entry in
the snapshot belongs to an object of another driver and is ignored
even when its content changed, since other drivers take and drop
references while the tests run. A concurrent dump holds references for
a moment, so the check waits for the candidates to drain. Attributing
by name means that a leak of an earlier run goes unnoticed and that an
object reusing the address of a freed one from the snapshot can hide a
leak, which is accepted, the entries carry nothing better to go by. A
second run alongside would have its devices attributed to this one,
which is one more reason for the lock every script takes.

Whether the tracker works is probed with a throwaway device, which has
to show entries while it is live and to drain them once it is deleted,
so that a kernel without the tracker is told apart from a broken
simulator and from a leak. The probe runs as the first test of the
stress script, which gains a final leak check, and of the new
dpll_reftrack.sh, which registers a device, removes it and verifies
that no reference survived. The snapshot file lives next to the lock
file under /run, for the same reason the lock file does not live in
/tmp. The library mounts debugfs when nothing else did, like it does
for configfs, and the selftest config enables the tracker together
with CONFIG_DEBUG_KERNEL, which it depends on, and debugfs.

Signed-off-by: Petr Oros <poros@redhat.com>
---
v2:
- keep the config file sorted and quote the ret expansions (netdev CI
  shellcheck)

v1: https://lore.kernel.org/all/20261103093910.596225-15-poros@redhat.com/
---
 .../selftests/drivers/net/dpll/Makefile       |   1 +
 .../testing/selftests/drivers/net/dpll/config |   3 +
 .../drivers/net/dpll/dpll_reftrack.sh         | 184 +++++++++++++
 .../selftests/drivers/net/dpll/dpll_stress.sh |  91 +++++-
 .../selftests/drivers/net/lib/sh/dpll_lib.sh  | 260 ++++++++++++++++--
 5 files changed, 517 insertions(+), 22 deletions(-)
 create mode 100755 tools/testing/selftests/drivers/net/dpll/dpll_reftrack.sh

diff --git a/tools/testing/selftests/drivers/net/dpll/Makefile b/tools/testing/selftests/drivers/net/dpll/Makefile
index 7cb42c93b0c9a1..2e8128dac8fa65 100644
--- a/tools/testing/selftests/drivers/net/dpll/Makefile
+++ b/tools/testing/selftests/drivers/net/dpll/Makefile
@@ -9,6 +9,7 @@ TEST_INCLUDES := \
 TEST_PROGS := \
 	dpll_basic.sh \
 	dpll_features.sh \
+	dpll_reftrack.sh \
 	dpll_stress.sh \
 # end of TEST_PROGS
 
diff --git a/tools/testing/selftests/drivers/net/dpll/config b/tools/testing/selftests/drivers/net/dpll/config
index e2c90b6970bd43..71931de5cfdb2e 100644
--- a/tools/testing/selftests/drivers/net/dpll/config
+++ b/tools/testing/selftests/drivers/net/dpll/config
@@ -1,3 +1,6 @@
 CONFIG_CONFIGFS_FS=y
+CONFIG_DEBUG_FS=y
+CONFIG_DEBUG_KERNEL=y
 CONFIG_DPLL=y
+CONFIG_DPLL_REFCNT_TRACKER=y
 CONFIG_DPLLSIM=m
diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_reftrack.sh b/tools/testing/selftests/drivers/net/dpll/dpll_reftrack.sh
new file mode 100755
index 00000000000000..8e624e8631776a
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_reftrack.sh
@@ -0,0 +1,184 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# DPLL reference tracking test. With CONFIG_DPLL_REFCNT_TRACKER=y the DPLL
+# core tracks device and pin references through ref_tracker; the debugfs
+# files dpll_device@<ptr> / dpll_pin@<ptr> list the stack traces of
+# currently unreleased references. This test records the entries that
+# exist before it starts, registers dpllsim devices and verifies that
+# every entry that appeared since is released again after unregistration.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../../net/lib.sh"
+source "$SCRIPTDIR/../lib/sh/dpll_lib.sh"
+
+ALL_TESTS="
+	test_ref_tracker_probe
+	test_basic_reftrack
+	test_final_leak_check
+"
+
+CREATED_DEVICE=""
+
+# The device stays recorded when its removal fails, so that the cleanup
+# handler tries again.
+destroy_device()
+{
+	[ -n "$CREATED_DEVICE" ] || return 0
+
+	dpllsim_delete_device "$CREATED_DEVICE" || return 1
+	CREATED_DEVICE=""
+}
+
+cleanup()
+{
+	# A second signal must not abort the teardown half way.
+	trap '' INT TERM
+
+	defer_scopes_cleanup
+	destroy_device || log_info "cannot delete device $CREATED_DEVICE"
+	# The device whose creation a signal interrupted is recorded by the
+	# library alone, a double delete of a tracked one is harmless.
+	if [ -n "$DPLLSIM_CREATED_NAME" ]; then
+		dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
+			log_info "cannot delete device $DPLLSIM_CREATED_NAME"
+	fi
+	if [ -n "$DPLLSIM_PROBE_DEVICE" ]; then
+		dpllsim_delete_device "$DPLLSIM_PROBE_DEVICE" ||
+			log_info "cannot delete device $DPLLSIM_PROBE_DEVICE"
+	fi
+	[ -n "$REF_SNAPSHOT" ] && rm -f "$REF_SNAPSHOT"
+	dpll_lib_cleanup
+}
+
+##############################################################################
+
+# Probe for the reference tracker and record the entries that exist
+# before the first test that creates devices, so that every entry that
+# appears from here on is attributed to this script. A probe device
+# that cannot be created says nothing about the tracker, the simulator
+# is broken. A probe device whose references do not drain is the kind
+# of leak the tests look for, found before the first test. One that
+# cannot be deleted is a failure too, it stays recorded for the cleanup
+# handler to try again. The tests skip when REF_TRACKER is 0 afterwards.
+test_ref_tracker_probe()
+{
+	RET=0
+
+	REF_TRACKER=0
+	if ! dpll_debugfs_available || ! [ -d "$DPLL_REF_TRACKER_DIR" ]; then
+		log_test_skip "DPLL ref_tracker probe" \
+			      "ref_tracker debugfs not available"
+		return
+	fi
+
+	# The snapshot file doubles as the scratch file of the probe, so
+	# it is needed before anything is recorded.
+	if ! REF_SNAPSHOT=$(dpll_ref_tracker_snapshot_file); then
+		check_err 1 "cannot create a temporary file for the snapshot"
+		log_test "DPLL ref_tracker probe"
+		return
+	fi
+
+	dpll_ref_tracker_probe "$REF_SNAPSHOT"
+	case $? in
+	0)
+		REF_TRACKER=1
+		;;
+	1)
+		log_test_skip "DPLL ref_tracker probe" \
+			      "CONFIG_DPLL_REFCNT_TRACKER not enabled"
+		return
+		;;
+	3)
+		REF_TRACKER=1
+		check_err 1 "references of the probe device not released"
+		;;
+	4)
+		check_err 1 "cannot delete the dpllsim probe device"
+		;;
+	*)
+		check_err 1 "cannot create dpllsim probe device"
+		;;
+	esac
+	dpll_ref_tracker_snapshot "$REF_SNAPSHOT"
+
+	log_test "DPLL ref_tracker probe"
+}
+
+test_basic_reftrack()
+{
+	RET=0
+
+	if [ "$REF_TRACKER" -eq 0 ]; then
+		log_test_skip "DPLL basic reference tracking" \
+			      "DPLL reference tracker not available"
+		return
+	fi
+
+	if ! dpllsim_setup_standard_device; then
+		check_err 1 "cannot create dpllsim device"
+		log_test "DPLL basic reference tracking"
+		return
+	fi
+	CREATED_DEVICE=$DPLLSIM_CREATED_NAME
+
+	destroy_device
+	check_err $? "cannot delete device $CREATED_DEVICE"
+
+	if ! dpll_device_wait_gone_by_clock_id "$DPLLSIM_CLOCK_ID" dpllsim; then
+		check_err 1 "device still on netlink after delete"
+	fi
+
+	dpll_ref_tracker_check_leaks "$REF_SNAPSHOT"
+	check_err $? "leaked references after device removal"
+
+	log_test "DPLL basic reference tracking"
+}
+
+test_final_leak_check()
+{
+	RET=0
+
+	if [ "$REF_TRACKER" -eq 0 ]; then
+		log_test_skip "DPLL final ref_tracker check" \
+			      "DPLL reference tracker not available"
+		return
+	fi
+	# A device an earlier test could not remove still holds its
+	# references, the failure to report is that one.
+	if [ -n "$CREATED_DEVICE" ]; then
+		log_test_skip "DPLL final ref_tracker check" \
+			      "device from an earlier test still present"
+		return
+	fi
+
+	dpll_ref_tracker_check_leaks "$REF_SNAPSHOT"
+	check_err $? "leaked DPLL references at end of test"
+
+	log_test "DPLL final ref_tracker check"
+}
+
+##############################################################################
+
+dpll_lib_check_prereqs
+# Another run would find the devices of this one left behind and see
+# them as leaks, see the lock in the library.
+dpll_lib_lock
+
+REF_SNAPSHOT=""
+REF_TRACKER=0
+# The module check and the probe test may mount configfs and debugfs, so
+# the exit trap that unmounts them again has to be in place before. INT
+# and TERM exit through it as well, so that a timeout imposed by the
+# harness still removes the device.
+trap cleanup EXIT
+trap 'exit 1' INT TERM
+dpllsim_check_module
+
+tests_run
+
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
index 2a6813871f34ea..9db6583ccfa938 100755
--- a/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/net/dpll/dpll_stress.sh
@@ -14,11 +14,13 @@ source "$SCRIPTDIR/../../../net/lib.sh"
 source "$SCRIPTDIR/../lib/sh/dpll_lib.sh"
 
 ALL_TESTS="
+	test_ref_tracker_probe
 	test_rapid_create_delete
 	test_concurrent_workers
 	test_pin_state_flooding
 	test_max_devices
 	test_deploy_cycle
+	test_ref_leaks
 "
 
 STRESS_ITERATIONS=${STRESS_ITERATIONS:-20}
@@ -153,6 +155,11 @@ cleanup()
 		dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
 			log_info "cannot delete device $DPLLSIM_CREATED_NAME"
 	fi
+	if [ -n "$DPLLSIM_PROBE_DEVICE" ]; then
+		dpllsim_delete_device "$DPLLSIM_PROBE_DEVICE" ||
+			log_info "cannot delete device $DPLLSIM_PROBE_DEVICE"
+	fi
+	[ -n "$REF_SNAPSHOT" ] && rm -f "$REF_SNAPSHOT"
 	dpll_lib_cleanup
 }
 
@@ -208,6 +215,60 @@ create_stress_device()
 
 ##############################################################################
 
+# Probe for the reference tracker and record the entries that exist
+# before the first test that creates devices, so that the final check
+# attributes every entry that appears from here on to this script. A
+# probe device that cannot be created says nothing about the tracker,
+# the simulator is broken. A probe device whose references do not drain
+# is the kind of leak the final check looks for, found before the first
+# test. One that cannot be deleted is a failure too, it stays recorded
+# for the cleanup handler to try again. REF_TRACKER tells the leak
+# check whether the tracker was seen at work.
+test_ref_tracker_probe()
+{
+	RET=0
+
+	REF_TRACKER=0
+	if ! dpll_debugfs_available || ! [ -d "$DPLL_REF_TRACKER_DIR" ]; then
+		log_test_skip "DPLL ref_tracker probe" \
+			      "ref_tracker debugfs not available"
+		return
+	fi
+
+	# The snapshot file doubles as the scratch file of the probe, so
+	# it is needed before anything is recorded.
+	if ! REF_SNAPSHOT=$(dpll_ref_tracker_snapshot_file); then
+		check_err 1 "cannot create a temporary file for the snapshot"
+		log_test "DPLL ref_tracker probe"
+		return
+	fi
+
+	dpll_ref_tracker_probe "$REF_SNAPSHOT"
+	case $? in
+	0)
+		REF_TRACKER=1
+		;;
+	1)
+		log_test_skip "DPLL ref_tracker probe" \
+			      "CONFIG_DPLL_REFCNT_TRACKER not enabled"
+		return
+		;;
+	3)
+		REF_TRACKER=1
+		check_err 1 "references of the probe device not released"
+		;;
+	4)
+		check_err 1 "cannot delete the dpllsim probe device"
+		;;
+	*)
+		check_err 1 "cannot create dpllsim probe device"
+		;;
+	esac
+	dpll_ref_tracker_snapshot "$REF_SNAPSHOT"
+
+	log_test "DPLL ref_tracker probe"
+}
+
 test_rapid_create_delete()
 {
 	local i clock_id name
@@ -462,16 +523,36 @@ test_deploy_cycle()
 	log_test "DPLL deploy/undeploy cycling"
 }
 
+test_ref_leaks()
+{
+	RET=0
+
+	# The probe test found the tracker inactive or could not run.
+	if [ "$REF_TRACKER" -eq 0 ]; then
+		log_test_skip "DPLL stress ref_tracker check" \
+			      "DPLL reference tracker not available"
+		return
+	fi
+
+	dpll_ref_tracker_check_leaks "$REF_SNAPSHOT"
+	check_err $? "leaked DPLL references after stress"
+
+	log_test "DPLL stress ref_tracker check"
+}
+
 ##############################################################################
 
 dpll_lib_check_prereqs
-# Another run would find the devices of this one left behind, see the
-# lock in the library.
+# Another run would find the devices of this one left behind and see
+# them as leaks, see the lock in the library.
 dpll_lib_lock
 
-# The module check may mount configfs, so the exit trap that unmounts it
-# again has to be in place before. INT and TERM exit through it as well,
-# so that a timeout imposed by the harness still removes the devices.
+REF_SNAPSHOT=""
+REF_TRACKER=0
+# The module check and the probe test may mount configfs and debugfs, so
+# the exit trap that unmounts them again has to be in place before. INT
+# and TERM exit through it as well, so that a timeout imposed by the
+# harness still removes the devices.
 trap cleanup EXIT
 trap 'exit 1' INT TERM
 dpllsim_check_module
diff --git a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
index ada10ee4394fd5..d851237bdac440 100644
--- a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
@@ -15,6 +15,8 @@
 
 CONFIGFS_DIR="/sys/kernel/config"
 DPLLSIM_CFG="$CONFIGFS_DIR/dpllsim"
+DEBUGFS_DIR="/sys/kernel/debug"
+DPLL_REF_TRACKER_DIR="$DEBUGFS_DIR/ref_tracker"
 
 # Next clock-id handed out by dpllsim_alloc_clock_id. The lookups of the
 # dpllsim helpers match on the module name as well, which is what keeps
@@ -52,12 +54,12 @@ dpll_lib_check_prereqs()
 }
 
 # The scripts that create dpllsim devices share the one dpllsim directory
-# and cannot run alongside each other, so each takes a lock on
-# DPLL_LOCK_FILE before it mounts or creates anything and skips when
-# another run holds it. The workers a script forks inherit the open file
-# and keep the lock until they exit. The file lives under /run rather
-# than in the world-writable /tmp, where a planted symlink could
-# redirect the open.
+# and the leak check attributes every new ref_tracker entry to the run,
+# so each takes a lock on DPLL_LOCK_FILE before it mounts or creates
+# anything and skips when another run holds it. The workers a script
+# forks inherit the open file and keep the lock until they exit. The
+# files live under /run rather than in the world-writable /tmp, where a
+# planted symlink could redirect the open.
 DPLL_RUN_DIR="/run/dpll-selftest"
 DPLL_LOCK_FILE="$DPLL_RUN_DIR/lock"
 DPLL_LOCK_FD=""
@@ -438,9 +440,10 @@ dpll_pin_set_phase_adjust()
 # structure (pins, clock_id, pin attributes) can only be changed while the
 # device is not live.
 #
-# dpllsim_available mounts configfs when nothing else did, which is the
-# one side effect the library has outside the dpllsim directory. Scripts
-# call dpll_lib_cleanup last from their exit trap to undo it.
+# dpllsim_available mounts configfs when nothing else did, like
+# dpll_debugfs_available below does for debugfs, which are the only side
+# effects the library has outside the dpllsim directory. Scripts call
+# dpll_lib_cleanup last from their exit trap to undo them.
 
 DPLLSIM_MOUNTED_CONFIGFS=0
 
@@ -456,24 +459,38 @@ dpllsim_available()
 	[ -d "$DPLLSIM_CFG" ]
 }
 
-# Unmount configfs again when dpllsim_available mounted it. Call after
-# the devices of the script are gone. A dpllsim device the script could
-# not remove keeps the mount, so that what is left can be looked at.
+__dpll_lib_umount()
+{
+	umount "$1" 2>/dev/null || log_info "cannot unmount $1"
+}
+
+# Unmount configfs and debugfs again when the library mounted them. Call
+# after the devices of the script are gone. A dpllsim device the script
+# could not remove keeps both mounted, so that what is left can be
+# looked at, the device in configfs and the references it still holds
+# in the ref_tracker entries of debugfs.
 dpll_lib_cleanup()
 {
-	local dev_path name
+	local dev_path name dir kept
+	local mounted=()
 
-	[ "$DPLLSIM_MOUNTED_CONFIGFS" -eq 1 ] || return 0
+	[ "$DPLLSIM_MOUNTED_CONFIGFS" -eq 1 ] && mounted+=("$CONFIGFS_DIR")
+	[ "$DPLL_MOUNTED_DEBUGFS" -eq 1 ] && mounted+=("$DEBUGFS_DIR")
 	DPLLSIM_MOUNTED_CONFIGFS=0
+	DPLL_MOUNTED_DEBUGFS=0
+	[ "${#mounted[@]}" -gt 0 ] || return 0
 
 	for dev_path in "$DPLLSIM_CFG"/*/; do
 		[ -d "$dev_path" ] || continue
 		name=$(basename "$dev_path")
-		log_info "device $name left behind, $CONFIGFS_DIR kept mounted"
+		kept=${mounted[*]}
+		kept="${kept/ / and } kept mounted"
+		log_info "device $name left behind, $kept"
 		return
 	done
-	umount "$CONFIGFS_DIR" 2>/dev/null ||
-		log_info "cannot unmount $CONFIGFS_DIR"
+	for dir in "${mounted[@]}"; do
+		__dpll_lib_umount "$dir"
+	done
 }
 
 dpllsim_check_module()
@@ -622,3 +639,212 @@ dpllsim_setup_standard_device()
 		return 1
 	fi
 }
+
+##############################################################################
+# ref_tracker leak checking
+#
+# With CONFIG_DPLL_REFCNT_TRACKER=y the DPLL core creates regular files
+# named dpll_device@<ptr> / dpll_pin@<ptr> under the ref_tracker debugfs
+# dir. Reading one prints the stack traces of currently unreleased
+# references; an empty (or removed) file means no leaks.
+#
+# The entries do not name their owner, so leaks are attributed by
+# snapshot, a non-empty entry whose name is not in the snapshot taken
+# before the test created its devices is a leak, an entry that is in it
+# belongs to another driver and is ignored even when its content changed.
+
+DPLL_MOUNTED_DEBUGFS=0
+# The snapshot files are kept in DPLL_RUN_DIR, see the lock above for
+# why that is not /tmp, as refsnap.<pid>.<random>.
+DPLL_REF_SNAPSHOT_PREFIX="$DPLL_RUN_DIR/refsnap"
+# Name of the probe device while it exists, so that the cleanup handler
+# of a script can remove it when the probe could not.
+DPLLSIM_PROBE_DEVICE=""
+
+# Create the file a snapshot is recorded in and print its name. The
+# scripts take the lock first, which creates DPLL_RUN_DIR. A run that
+# is killed with SIGKILL cannot remove its file, so the files of earlier
+# runs whose process is gone are removed first, the pid of the run is
+# the component after the first dot of the name. A pid that was reused
+# by a process that lives long would keep such a file forever, so files
+# older than a day are removed whatever their pid is. Return 1 when the
+# file cannot be created.
+dpll_ref_tracker_snapshot_file()
+{
+	local file pid
+
+	find "$DPLL_RUN_DIR" -maxdepth 1 \
+	     -name "${DPLL_REF_SNAPSHOT_PREFIX##*/}.*" -mmin +1440 -delete \
+	     2>/dev/null
+	for file in "$DPLL_REF_SNAPSHOT_PREFIX".*; do
+		[ -f "$file" ] || continue
+		pid=${file#"$DPLL_REF_SNAPSHOT_PREFIX".}
+		pid=${pid%%.*}
+		[[ "$pid" =~ ^[0-9]+$ ]] || continue
+		[ -d "/proc/$pid" ] || rm -f "$file"
+	done
+	mktemp "$DPLL_REF_SNAPSHOT_PREFIX.$$.XXXXXX"
+}
+
+# Mount debugfs when nothing else did, so that the ref_tracker entries
+# can be read, and remember that for dpll_lib_cleanup. Return 0 when
+# debugfs is mounted afterwards, whether the tracker dir exists is for
+# the caller to check.
+dpll_debugfs_available()
+{
+	if ! mountpoint -q "$DEBUGFS_DIR"; then
+		mount -t debugfs none "$DEBUGFS_DIR" &> /dev/null &&
+			DPLL_MOUNTED_DEBUGFS=1
+	fi
+	mountpoint -q "$DEBUGFS_DIR"
+}
+
+__dpll_ref_tracker_hash()
+{
+	printf '%s' "$1" | md5sum | cut -d' ' -f1
+}
+
+# Write "<name> <content hash>" for every existing dpll_device@* /
+# dpll_pin@* entry to the given file, one entry per line. The file ends
+# up empty when the tracker dir is missing.
+dpll_ref_tracker_snapshot()
+{
+	local file=$1
+	local entry content
+
+	for entry in "$DPLL_REF_TRACKER_DIR"/dpll_device@* \
+		     "$DPLL_REF_TRACKER_DIR"/dpll_pin@*; do
+		[ -f "$entry" ] || continue
+		content=$(cat "$entry" 2>/dev/null)
+		echo "$(basename "$entry")" \
+		     "$(__dpll_ref_tracker_hash "$content")"
+	done > "$file"
+}
+
+# Print the leak candidates, the non-empty entries whose name is not in
+# the snapshot, each followed by its content. Prints nothing when there
+# is no candidate.
+__dpll_ref_tracker_new_entries()
+{
+	local file=$1
+	local entry name content
+
+	for entry in "$DPLL_REF_TRACKER_DIR"/dpll_device@* \
+		     "$DPLL_REF_TRACKER_DIR"/dpll_pin@*; do
+		[ -f "$entry" ] || continue
+		name=$(basename "$entry")
+		grep -q "^$name " "$file" && continue
+		content=$(cat "$entry" 2>/dev/null)
+		[ -n "$content" ] || continue
+		echo "Unreleased references in $name:"
+		echo "$content"
+	done
+}
+
+# Return 0 when there is no leak candidate. Otherwise print the
+# candidates and return 1, busywait hands the output of its last failed
+# check to the caller when it times out.
+__dpll_ref_tracker_settled()
+{
+	local leaks
+
+	leaks=$(__dpll_ref_tracker_new_entries "$1")
+	[ -z "$leaks" ] && return 0
+
+	echo "$leaks"
+	return 1
+}
+
+# Log the entries of the snapshot whose content changed since and record
+# the new content, so that the same change is logged once.
+__dpll_ref_tracker_note_changed()
+{
+	local file=$1
+	local entry name content hash changed=""
+
+	for entry in "$DPLL_REF_TRACKER_DIR"/dpll_device@* \
+		     "$DPLL_REF_TRACKER_DIR"/dpll_pin@*; do
+		[ -f "$entry" ] || continue
+		name=$(basename "$entry")
+		grep -q "^$name " "$file" || continue
+		content=$(cat "$entry" 2>/dev/null)
+		hash=$(__dpll_ref_tracker_hash "$content")
+		grep -qx "$name $hash" "$file" && continue
+		sed -i "s/^$name .*/$name $hash/" "$file"
+		changed="$changed $name"
+	done
+
+	[ -n "$changed" ] && log_info \
+		"references of another driver changed (ignored):$changed"
+	return 0
+}
+
+# $1: snapshot file written by dpll_ref_tracker_snapshot before the
+# objects under test were created. Return 0 when no reference to an
+# object created since the snapshot is held any more. A concurrent dump
+# holds references for a moment, so wait for the candidates to drain. A
+# timeout means the references never drained, the candidates seen by the
+# last check are then printed and count as leaked.
+dpll_ref_tracker_check_leaks()
+{
+	local file=$1
+	local leaks ret=0
+
+	[ -d "$DPLL_REF_TRACKER_DIR" ] || return 0
+
+	if ! leaks=$(busywait "$BUSYWAIT_TIMEOUT" __dpll_ref_tracker_settled \
+			      "$file"); then
+		echo "References not released within $BUSYWAIT_TIMEOUT ms:"
+		echo "$leaks"
+		ret=1
+	fi
+	__dpll_ref_tracker_note_changed "$file"
+
+	return "$ret"
+}
+
+# Probe whether the kernel tracks DPLL references with a throwaway
+# dpllsim device, the entries that exist before it are recorded in the
+# scratch file $1. Return 0 when the tracker works, 1 when no entry
+# appeared (no CONFIG_DPLL_REFCNT_TRACKER), 2 when the probe device
+# could not be created or deployed, 3 when its entries did not drain
+# after its deletion and 4 when it could not be deleted, its name then
+# stays in DPLLSIM_PROBE_DEVICE for the cleanup handler to retry. Must
+# not be called from a subshell, it allocates clock-ids.
+dpll_ref_tracker_probe()
+{
+	local file=$1
+	local name attempt ret=2
+
+	dpll_ref_tracker_snapshot "$file"
+
+	# A directory of that name left behind by an earlier run takes a
+	# fresh clock-id, anything else is a broken simulator.
+	for attempt in 1 2 3; do
+		dpllsim_alloc_clock_id
+		name="probe$DPLLSIM_CLOCK_ID"
+		dpllsim_create_device "$name" "$DPLLSIM_CLOCK_ID" && break
+		[ -d "$DPLLSIM_CFG/$name" ] || return 2
+		[ "$attempt" -lt 3 ] || return 2
+	done
+	DPLLSIM_PROBE_DEVICE=$name
+
+	if dpllsim_add_pin "$name" ext0 ext 10000000 input &&
+	   dpllsim_deploy "$name"; then
+		ret=1
+		[ -n "$(__dpll_ref_tracker_new_entries "$file")" ] && ret=0
+	fi
+
+	if dpllsim_delete_device "$name"; then
+		DPLLSIM_PROBE_DEVICE=""
+	else
+		log_info "cannot delete device $name"
+		return 4
+	fi
+	if [ "$ret" -eq 0 ] &&
+	   ! busywait "$BUSYWAIT_TIMEOUT" __dpll_ref_tracker_settled "$file" \
+		      > /dev/null; then
+		ret=3
+	fi
+	return "$ret"
+}
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 05/14] dpll: dpllsim: Add support to set input pin state
  2026-11-05 15:18 ` [PATCH net-next v2 05/14] dpll: dpllsim: Add support to set input pin state Petr Oros
@ 2026-11-06 10:41   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:41 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Add support for setting the state of input pins on the device.
> Implement the state_on_dpll_set callback for input pins and advertise
> the state-can-change capability for them, so that userspace can move a
> pin between connected, disconnected and selectable. The driver stores
> the requested state and reports it back unchanged, it does not act on it
> yet. Output pins keep reporting their state only.
> 
> Signed-off-by: Petr Oros <poros@redhat.com>
> ---
>   drivers/dpll/dpllsim/dpll.c | 19 +++++++++++++++++++
>   1 file changed, 19 insertions(+)
> 
> diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
> index a1d5921fb8d017..2b0c053a7b6087 100644
> --- a/drivers/dpll/dpllsim/dpll.c
> +++ b/drivers/dpll/dpllsim/dpll.c
> @@ -104,10 +104,23 @@ static int dpllsim_pin_state_get(const struct dpll_pin *pin, void *pin_priv,
>   	return 0;
>   }
>   
> +static int dpllsim_pin_state_set(const struct dpll_pin *pin, void *pin_priv,
> +				 const struct dpll_device *dpll,
> +				 void *dpll_priv, enum dpll_pin_state state,
> +				 struct netlink_ext_ack *extack)
> +{
> +	struct dpllsim_pin *sim_pin = pin_priv;
> +
> +	guard(mutex)(&sim_pin->dev->lock);
> +	sim_pin->state = state;
> +	return 0;
> +}

Should DPLL_PIN_STATE_CONNECTED be accepted here?

The device always reports automatic mode, which dpllsim_make_group() sets
and nothing changes:

	sim->mode = DPLL_MODE_AUTOMATIC;

Documentation/driver-api/dpll.rst limits what a user can request in that
mode:

     In automatic mode (``DPLL_MODE_AUTOMATIC``) the user can request one of
     following pin states:

     - ``DPLL_PIN_STATE_SELECTABLE`` - the pin shall be considered as valid
       input for automatic selection algorithm
     - ``DPLL_PIN_STATE_DISCONNECTED`` - the pin shall be not considered as
       a valid input for automatic selection algorithm

     Pins that have the ``DPLL_PIN_CAPABILITIES_STATE_CONNECTED_OVERRIDE``
     capability can additionally be set to ``DPLL_PIN_STATE_CONNECTED`` in
     automatic mode, overriding the active input selection.

dpll_pin_state_set() in the core only checks state-can-change, so the
driver has to refuse the state. zl3073x does that for its automatic mode
in zl3073x_dpll_input_pin_state_on_dpll_set():

	case ZL_DPLL_MODE_REFSEL_MODE_AUTO:
		if (state == DPLL_PIN_STATE_SELECTABLE) {
			...
		} else if (state == DPLL_PIN_STATE_DISCONNECTED) {
			...
		} else {
			goto invalid_state;
		}

[ ... ]
> @@ -149,6 +162,12 @@ static void dpllsim_pin_props(struct dpllsim_pin *pin,
>   		prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_ext);
>   		break;
>   	}
> +
> +	/* Capabilities follow the ops the pin is served by */
> +	if (dpllsim_pin_select_ops(pin) == &dpllsim_input_pin_ops)
> +		prop->capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
> +	else
> +		prop->capabilities = 0;
>   }
The input pins advertise state-can-change only. If connected is meant to
be settable on this automatic-only device, should
DPLL_PIN_CAPABILITIES_STATE_CONNECTED_OVERRIDE be advertised here as well?

If not, should dpllsim_pin_state_set() return -EINVAL with an extack
message for it?

The commit message says "so that userspace can move a pin between
connected, disconnected and selectable", and the stress test added by the
next patch, 70a4dcf52b9c ("selftests: dpll: add pin state tests"), relies
on it. test_pin_state_flooding() cycles through

	local states=(connected disconnected selectable)

and fails when one of the sets is refused.

Thanks,
Ivan


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 14/14] selftests: dpll: add reference leak tests
  2026-11-05 15:18 ` [PATCH net-next v2 14/14] selftests: dpll: add reference leak tests Petr Oros
@ 2026-11-06 10:47   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:47 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> With CONFIG_DPLL_REFCNT_TRACKER the DPLL core keeps the stack traces of
> unreleased device and pin references in debugfs. The entries do not
> name their owner, so add library helpers that snapshot the entries
> present before a test creates its devices and fail when an entry that
> is not in the snapshot still holds references afterwards. An entry in
> the snapshot belongs to an object of another driver and is ignored
> even when its content changed, since other drivers take and drop
> references while the tests run. A concurrent dump holds references for
> a moment, so the check waits for the candidates to drain. Attributing
> by name means that a leak of an earlier run goes unnoticed and that an
> object reusing the address of a freed one from the snapshot can hide a
> leak, which is accepted, the entries carry nothing better to go by. A
> second run alongside would have its devices attributed to this one,
> which is one more reason for the lock every script takes.
> 
> Whether the tracker works is probed with a throwaway device, which has
> to show entries while it is live and to drain them once it is deleted,
> so that a kernel without the tracker is told apart from a broken
> simulator and from a leak. The probe runs as the first test of the
> stress script, which gains a final leak check, and of the new
> dpll_reftrack.sh, which registers a device, removes it and verifies
> that no reference survived. The snapshot file lives next to the lock
> file under /run, for the same reason the lock file does not live in
> /tmp. The library mounts debugfs when nothing else did, like it does
> for configfs, and the selftest config enables the tracker together
> with CONFIG_DEBUG_KERNEL, which it depends on, and debugfs.
> 
> Signed-off-by: Petr Oros <poros@redhat.com>

Which dump holds a reference here?

The references to a dpll_device or a dpll_pin are taken in
dpll_device_get(), dpll_device_register(), dpll_pin_get(),
fwnode_dpll_pin_find() and by the pin registration, in
dpll_xa_ref_pin_add() and dpll_xa_ref_dpll_add().

The netlink dump and doit handlers in drivers/dpll/dpll_netlink.c run
under dpll_lock and do not take one.

Could the sentence name what actually holds the references the check waits
for, or be dropped if nothing does? The same sentence is in the comment
above dpll_ref_tracker_check_leaks():

[...]

> diff --git a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
> index ada10ee4394fd5..d851237bdac440 100644
> --- a/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh
> +++ b/tools/testing/selftests/drivers/net/lib/sh/dpll_lib.sh

[...]

> @@ -622,3 +639,212 @@ dpllsim_setup_standard_device()

[...]
> +# $1: snapshot file written by dpll_ref_tracker_snapshot before the
> +# objects under test were created. Return 0 when no reference to an
> +# object created since the snapshot is held any more. A concurrent dump
> +# holds references for a moment, so wait for the candidates to drain. A
> +# timeout means the references never drained, the candidates seen by the
> +# last check are then printed and count as leaked.
> +dpll_ref_tracker_check_leaks()
[...]

Thanks,
Ivan


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 01/14] dpll: dpllsim: Add simulated DPLL device driver
  2026-11-05 15:18 ` [PATCH net-next v2 01/14] dpll: dpllsim: Add " Petr Oros
@ 2026-11-06 10:47   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:47 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Add dpllsim, a driver providing simulated DPLL devices for testing the
> DPLL subsystem and its userspace tooling without real hardware, in the
> same spirit as netdevsim for network devices. The DPLL core identifies
> a device by clock ID, index and module only, so the driver needs no bus
> and no struct device at all.
> 
> Instances are created and configured at runtime through configfs,
> following the gpio-sim precedent for objects that userspace creates on
> demand. Creating a directory under/sys/kernel/config/dpllsim/ allocates
> an instance, its clock_id attribute selects the clock ID, writing 1 to
> live registers the DPLL device with the core and writing 0 unregisters
> it again. Removing the directory tears a live instance down implicitly.
> Keeping registration separate from creation lets a test register and
> unregister the same device repeatedly, which is where the core bugs the
> driver is meant to catch tend to live, and it allows the structural
> attributes to be refused with EBUSY while the device is visible to
> userspace. Unlike sysfs, configfs does not imply a stable ABI, which
> suits a driver whose knobs exist only to drive tests.
> 
> Reading live reports whether the device is registered with the DPLL
> subsystem, it turns 1 only once the whole live transition succeeded and
> 0 again as the first step of the teardown. The configuration is frozen
> earlier, from the moment the transition obtains the device handle, so
> that a configuration write racing with the transition is refused rather
> than lost.
> 
> The device registers as a PPS type DPLL with the two mandatory callbacks
> only. mode_get reports automatic mode and lock_status_get reports
> unlocked with no error. Pins come with the following patches and the
> simulation of lock acquisition with a later series.
> 
> Document the configfs layout under Documentation/driver-api and add a
> MAINTAINERS entry.
> 
> Signed-off-by: Petr Oros<poros@redhat.com>

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 02/14] selftests: dpll: add test library and device lifecycle tests
  2026-11-05 15:18 ` [PATCH net-next v2 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
@ 2026-11-06 10:48   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:48 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Add a shell library for DPLL selftests with wrappers around the iproute2
> dpll tool and around the dpllsim configfs interface, and the first two
> test scripts built on it. The library resolves devices by the clock ID
> chosen at creation time, since netlink ids depend on what else exists
> on the system, and the dpllsim helpers restrict the lookup to their
> module, so that a device of another driver with the same clock ID is
> never picked up. Every query runs the tool first and jq on what it
> printed, so that a failed query is told apart from a missing attribute.
> The library mounts configfs when nothing else did, and the exit trap of
> each script undoes that, removes the devices the script created and
> runs on INT and TERM as well, so that a timeout imposed by the harness
> still cleans up.
> 
> Two runs of scripts that create dpllsim devices would share the one
> dpllsim directory and could draw the same clock IDs, so every such
> script takes a lock before it mounts or creates anything and skips when
> another run holds it. The lock file lives under /run rather than in
> /tmp, since the scripts run as root and open the file for writing, and
> a symlink planted in the world-writable /tmp would redirect that open.
> 
> dpll_basic.sh checks that a device reports the mandatory attributes
> over netlink. It runs against the device named by DPLL_DEV_ID or the
> first device that is not a dpllsim one, and creates a dpllsim device
> when there is none, so the same script serves the simulator and real
> drivers. dpll_stress.sh is simulator only and exercises the lifecycle
> paths of the core by creating and deleting devices in a loop and by
> cycling one device through repeated register and unregister
> transitions. The dpllsim MAINTAINERS entry covers the new directory
> and the shared library.
> 
> Signed-off-by: Petr Oros <poros@redhat.com>
Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 03/14] dpll: dpllsim: Add configurable pins
  2026-11-05 15:18 ` [PATCH net-next v2 03/14] dpll: dpllsim: Add configurable pins Petr Oros
@ 2026-11-06 10:48   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:48 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Pins are the unit of most DPLL netlink interaction, so let a device
> instance carry an arbitrary set of them. Each directory created under
> the pins/ group of an instance is one pin with type, frequency and
> direction attributes. The type takes the names of the DPLL pin type
> enum as iproute2 prints them, the direction is input or output and the
> frequency must be one of the frequencies the driver advertises as
> supported for the type. The pin directory name becomes the board label
> of the pin, which gives tests a stable handle to find a pin by. The
> attributes are writable only while the instance is not live and return
> EBUSY otherwise.
> 
> At live=1 the pins are ordered as created and each pin records its
> index in that order, which is the pin index it is registered under.
> Their frequencies are validated against the per type tables, a MUX pin
> configured as output is rejected since a MUX pin stands for an input
> selector, and all pin handles are obtained up front, so that a failure
> leaves nothing half visible. The device is registered first and the
> pins after it, so that every pin registration event reaches userspace
> with a valid device, and a failure unwinds exactly what was registered
> in reverse order. While the instance is live the pin items are held
> with configfs_depend_item_unlocked(), which makes rmdir of a pin fail
> with EBUSY until the instance is taken down.
> 
> The pins register with the mandatory callbacks only, direction_get and
> state_on_dpll_get, plus frequency_get since the frequency is known from
> the configuration. The input and output ops tables are identical here
> and diverge when input pins gain their setters in the next patches. The
> state is always reported as disconnected for now, the settable pin
> features follow in separate patches.
> 
> Signed-off-by: Petr Oros<poros@redhat.com>

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 04/14] selftests: dpll: add pin enumeration and property tests
  2026-11-05 15:18 ` [PATCH net-next v2 04/14] selftests: dpll: add pin enumeration and property tests Petr Oros
@ 2026-11-06 10:48   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:48 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Extend the library with pin queries. Pins of a device are collected
> from the full pin dump with jq, since older iproute2 lacks the device
> filter of "pin show", and the fields that live in the parent-device
> nest are read through a helper that selects the nest of the device
> under test. The helpers that work on the full dump normalize its shape
> the same way the id-based ones do, so that a tool printing a flat
> object instead of the dump wrapper works with all of them. Like the
> device queries, every pin query runs the tool first and jq on what it
> printed, so that a query which fails returns the status of the tool
> instead of the empty output a missing attribute produces too. The
> prerequisite check now also requires a tool that knows "pin show", so
> that a tool too old for the pin queries is a skip up front rather than
> a failed query in every test. The dpllsim helpers gain
> dpllsim_add_pin, which creates a pin directory and writes its type,
> frequency and direction, and the standard device grows a GNSS input,
> an external input and an external output.
> 
> dpll_basic.sh checks that the enumeration yields a numeric id for every
> pin of the device, the ids are taken from the dump as they are and jq
> prints null for a pin without one, that every pin has an id and a type
> and carries the direction in the parent-device nest, and that an input
> pin reports a valid state. The property test does not go on to query
> pins by an id that is not one, and reports a pin it cannot query at all
> as one failure rather than as one per attribute. The frequency is not
> required, a pin without a frequency callback does not report one. The
> device the script created has the standard pins, so an empty pin list
> or a missing input pin on it means a failed query or broken tooling
> rather than a pinless device. The library gains
> dpll_log_test_skip_or_fail for such conditions, which fails on the
> device the script created, recorded in DPLL_CREATED_DEVICE next to
> DPLL_DEV_ID, and skips on a device the user provided. The stress script
> creates its devices with a random set of pins of every type and both
> directions, MUX pins always as inputs, and the deploy cycle test runs
> with the standard pin set.
> 
> Signed-off-by: Petr Oros<poros@redhat.com>

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 06/14] selftests: dpll: add pin state tests
  2026-11-05 15:18 ` [PATCH net-next v2 06/14] selftests: dpll: add pin state tests Petr Oros
@ 2026-11-06 10:49   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:49 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Add dpll_features.sh, a script that exercises individual device and pin
> callbacks with get/set roundtrips over netlink. It creates a dpllsim
> device with the standard pin layout by default or runs against the
> device named by DPLL_DEV_ID, and picks the pin to reconfigure up front,
> the first input pin that is not connected on the device, so that no
> test touches the reference the device is locked to. The first test
> moves that pin between the disconnected and selectable states, reads
> the state back after each step and restores the original state at the
> end. It never connects the pin, which could make it the reference.
> 
> A driver may refuse a state the capability promises, for a constraint
> the interface does not tell. That is a skip on a device provided by
> the user, with the reason the tool reported, and a failure on the
> device the script created, whose simulator accepts both states. The
> restore is deferred right after the original state is read, and the
> cleanup handler of every script that defers runs the pending restores
> before it removes the devices the script created, so that an
> interrupted run puts the device back as well.
> 
> The stress script gains a test that floods one input pin of a fresh
> device with state changes. The library gains the state setter both
> use, which keeps the extack line of the tool in DPLL_LAST_ERR so that
> a test can tell a refusal from a failure, and a check whether the pin
> set usage of the tool knows a keyword, so that a test skips on a tool
> too old for it rather than every script requiring a newer tool.
> 
> Signed-off-by: Petr Oros<poros@redhat.com>

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 07/14] dpll: dpllsim: Add support to get/set priority on input pins
  2026-11-05 15:18 ` [PATCH net-next v2 07/14] dpll: dpllsim: Add support to get/set priority on input pins Petr Oros
@ 2026-11-06 10:49   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:49 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Add support for getting and setting the priority of input pins.
> Implement the prio_get and prio_set callbacks and advertise the
> priority-can-change capability for input pins, which includes the MUX
> type pins since those are always inputs.
> 
> The initial priority of a pin is configured through a new prio
> attribute of the pin directory, which takes a value between 0 and 255
> and defaults to 128, so that a test can lay out the reference order
> before the device goes live. Like the other pin attributes it is
> writable only while the device is not live. Priority changes made over
> netlink are stored and reported back, they will drive the automatic
> reference selection once the simulation loop exists. The setter
> rejects a value above 255 with EINVAL and an extack message. A priority
> is meaningful for input pins only, so live fails with EINVAL when an
> output pin was given a priority other than the default, next to the
> frequency validation.
> 
> Signed-off-by: Petr Oros<poros@redhat.com>

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 08/14] selftests: dpll: add pin priority test
  2026-11-05 15:18 ` [PATCH net-next v2 08/14] selftests: dpll: add pin priority test Petr Oros
@ 2026-11-06 10:50   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:50 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Add a test that sets a new priority on an input pin, reads it back and
> restores the original value. The range of priorities a device accepts is
> not advertised over netlink, so the test tries one below the current
> value and then the small values every driver accepts, since a large
> current value may mark an unused pin outside the settable range, as the
> ice driver does with 255. A driver may still refuse all of them for a
> constraint the interface does not tell, so that is a skip on a device
> provided by the user, with the reason the tool reported, and a failure
> on the device the script created, whose simulator accepts every
> priority. The test skips when the dpll tool lacks the prio keyword of
> pin set, and the restore is deferred right after the original value is
> read so that an interrupted run restores it too. A pin that does not
> advertise the priority-can-change capability or does not report a
> priority is a skip on a device provided by the user and a failure on the
> device the script created, whose pins have both.
> 
> The dpllsim_add_pin helper gains an optional priority argument, which is
> written only when it is given, so that a pin without one keeps the
> default of the simulator. A priority is accepted for input pins only,
> so the callers pass one for those alone.
> 
> The standard device lays its inputs out as gnss0 with priority 0 and
> ext0 with priority 1, and the stress devices get random priorities on
> their input pins.
> 
> Signed-off-by: Petr Oros<poros@redhat.com>

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 09/14] dpll: dpllsim: Add support to set frequency on pins
  2026-11-05 15:18 ` [PATCH net-next v2 09/14] dpll: dpllsim: Add support to set frequency on pins Petr Oros
@ 2026-11-06 10:50   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:50 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Add support to set the frequency of input and output pins. The DPLL
> core validates the requested value against the supported frequency
> list the pin advertises, so the frequency_set callback only has to
> store it. The same per type tables that validate the configured
> frequency at live time serve as the supported list. The configfs
> attribute is rejected with EBUSY while the device is live, so only the
> netlink setter changes the frequency of a registered pin and the two
> cannot race.
> 
> Signed-off-by: Petr Oros<poros@redhat.com>

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 10/14] selftests: dpll: add pin frequency test
  2026-11-05 15:18 ` [PATCH net-next v2 10/14] selftests: dpll: add pin frequency test Petr Oros
@ 2026-11-06 10:51   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:51 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Add a test that picks a supported frequency other than the current one
> from the frequency-supported list of an input pin, sets it, reads it
> back and restores the original value. Both ends of every advertised
> range are candidates, so that a pin advertising a single range wider
> than one value still has one. It skips when the dpll tool lacks the
> frequency keyword of pin set, and the restore is deferred right after
> the original value is read so that an interrupted run restores it too.
> A pin that reports no frequency or advertises a single frequency only
> is a skip on a device provided by the user and a failure on the device
> the script created, whose pins advertise several.
> 
> The driver may refuse the candidate. A driver without a frequency
> setter refuses every value with EOPNOTSUPP, and a driver with
> constraints of its own refuses values the advertised ranges allow,
> zl3073x for instance wants the frequencies of an output pair to divide
> each other. On a device provided by the user a refused set is a skip
> that names the reason the tool reported, which the setter keeps in
> DPLL_LAST_ERR, on the created device the simulator accepts every
> advertised value and the refusal is a failure. Once the set went
> through, a refused restore is a failure on either.
> 
> Signed-off-by: Petr Oros<poros@redhat.com>

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 11/14] dpll: dpllsim: Add support to adjust phase
  2026-11-05 15:18 ` [PATCH net-next v2 11/14] dpll: dpllsim: Add support to adjust phase Petr Oros
@ 2026-11-06 10:51   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:51 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Add support to get and set the phase adjustment of input and output
> pins. The pins advertise a granularity of 100 ps and a symmetric range
> whose ends are the largest multiples of the granularity that fit a
> 32-bit signed value, so that the advertised ends themselves are
> settable, the core validates the requested value against both and the
> driver stores it. The adjustment is reset to zero every time the device
> goes live, so that repeated deploy cycles start from a known state. It
> becomes the baseline of the simulated phase offset measurements once
> those exist.
> 
> Signed-off-by: Petr Oros<poros@redhat.com>

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 12/14] selftests: dpll: add pin phase adjust test
  2026-11-05 15:18 ` [PATCH net-next v2 12/14] selftests: dpll: add pin phase adjust test Petr Oros
@ 2026-11-06 10:51   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:51 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Add a test that reads the phase adjustment range and granularity a
> pin advertises, picks the largest multiple of the granularity inside
> the range, or the smallest one when that is already the current value,
> sets it, reads it back and restores the original value. It skips when
> the dpll tool lacks the phase-adjust keyword of pin set and when the
> range is too narrow to pick a different value. A pin that reports no
> phase adjustment or no range is a skip on a device provided by the user
> and a failure on the device the script created, whose pins report
> both.
> 
> The core refuses a value that is not a multiple of the granularity a
> driver reports, the current value included, so a pin that sits at such
> a value could not be put back. The test bails out on that before it
> schedules the restore, which is deferred right after so that an
> interrupted run restores the value too.
> 
> The granularity is only reported by drivers that have the core enforce
> one. Without it the target is computed with a granularity of 1, and a
> driver may refuse it for a granularity of its own or for any other
> constraint the interface does not advertise. A refused set is therefore
> a skip on a device provided by the user, with the reason the tool
> reported, and a failure on the device the script created, whose
> simulator accepts every value its range and granularity allow. The
> test ends there, a value that was not set has nothing to read back.
> 
> Signed-off-by: Petr Oros <poros@redhat.com>
Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 13/14] selftests: dpll: add concurrency stress tests
  2026-11-05 15:18 ` [PATCH net-next v2 13/14] selftests: dpll: add concurrency stress tests Petr Oros
@ 2026-11-06 10:52   ` Ivan Vecera
  0 siblings, 0 replies; 30+ messages in thread
From: Ivan Vecera @ 2026-11-06 10:52 UTC (permalink / raw)
  To: Petr Oros, netdev
  Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

On 11/5/26 16:18, Petr Oros wrote:
> Extend the stress script with tests that register and unregister
> devices from several processes at once and that keep a number of
> devices with many pins live at the same time, checking that every one
> of them is visible on netlink.
> 
> The workers run in subshells, so the parent cannot track the device a
> worker has in flight. Each worker removes it from its own exit trap
> instead, and the parent reserves a range of clock IDs for every worker,
> so that the devices of a worker cannot collide with those of another
> worker or of the parent and whatever a killed worker left behind can
> be removed by name, at the end of the test and from the cleanup
> handler. The handler stops the recorded workers together with the jobs
> of the shell, gives them a few seconds to run their exit traps before
> forcing the survivors, and goes on without a worker that does not die,
> since one stuck in the kernel would hang the teardown. Deleting a
> device treats a part of it that is gone by the time its step runs as
> removed, so that a removal running concurrently with the one of a
> worker does not turn into a spurious failure. The workers inherit the
> open lock file of the library, so the lock is held until the script
> and every worker have exited.
> 
> Signed-off-by: Petr Oros<poros@redhat.com>

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver
  2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
                   ` (13 preceding siblings ...)
  2026-11-05 15:18 ` [PATCH net-next v2 14/14] selftests: dpll: add reference leak tests Petr Oros
@ 2026-11-06 12:27 ` Jiri Pirko
  14 siblings, 0 replies; 30+ messages in thread
From: Jiri Pirko @ 2026-11-06 12:27 UTC (permalink / raw)
  To: Petr Oros
  Cc: netdev, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
	Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Ivan Vecera,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, linux-doc, linux-kselftest

Thu, Nov 05, 2026 at 04:18:38PM +0100, poros@redhat.com wrote:
>The DPLL subsystem has no in-tree driver that can run without real
>hardware, so core changes and userspace tooling are hard to exercise in
>CI, and the pin-on-pin and reference sync paths see almost no coverage
>outside the vendors' labs. This series adds dpllsim, a simulated DPLL
>device driver, together with selftests that run against it.
>
>The driver is controlled through configfs, following gpio-sim. Writing a
>clock ID into a device directory and setting "live" to 1 registers a
>DPLL device with the core. Pins are directories under pins/ with their
>type, frequency, direction and priority as attributes, and all of them
>are registered when the device goes live. The split between creating
>the configuration and going live keeps the register and unregister
>paths reachable from userspace, so they can be exercised repeatedly
>without reloading the module. Nothing in the simulator is a stable ABI.
>
>The patches follow the shape of the zl3073x introduction. The
>driver starts with the device alone, then gains pins and the static pin
>operations one at a time (state, priority, frequency, phase adjustment),
>and every feature patch is followed by the selftest that covers it. The
>last two patches add concurrency stress tests and reference tracker
>leak tests for the lifecycle paths.
>
>The simulation loop (lock state machine, phase and frequency
>measurements, monitoring features), the topology features (MUX pin
>hierarchies, reference sync, media failure injection), netdevsim
>integration and device mode setting follow in two further series once
>this one is in, to stay within the 15 patch limit.
>
>The driver and the selftests were developed with an LLM coding
>assistant (Claude Code) working under my direction. I chose the
>configfs design, the split into one feature per patch and the test
>scenarios, and I reviewed every line. The assistant wrote most of the
>code and the first drafts of the changelogs, and it found and fixed
>bugs by running the suite on the lab host. The series then went
>through several rounds of an LLM based patch reviewer whose findings
>were fixed, and through a mutation test in which deliberately broken
>driver and core code was caught by the tests.
>
>The selftests live in tools/testing/selftests/drivers/net/dpll and run
>with "make -C tools/testing/selftests TARGETS=drivers/net/dpll
>run_tests". By default they create dpllsim devices; with
>DPLL_DEV_ID=<id> the basic and feature scripts run against an existing
>device instead, so the same suite exercises real hardware. The whole
>suite was run through the kselftest runner on an ice E810 host with
>CONFIG_KASAN, CONFIG_PROVE_LOCKING and CONFIG_DPLL_REFCNT_TRACKER
>enabled, against the simulator and, with DPLL_DEV_ID, against the ice
>DPLL, where the tests that need a simulator skip and the pin state and
>priorities of the hardware are left as they were. All pass with no
>warnings in dmesg.
>

set-
Acked-by: Jiri Pirko <jiri@nvidia.com>

^ permalink raw reply	[flat|nested] 30+ messages in thread

end of thread, other threads:[~2026-11-06 12:27 UTC | newest]

Thread overview: 30+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-05 15:18 [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Petr Oros
2026-11-05 15:18 ` [PATCH net-next v2 01/14] dpll: dpllsim: Add " Petr Oros
2026-11-06 10:47   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
2026-11-06 10:48   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 03/14] dpll: dpllsim: Add configurable pins Petr Oros
2026-11-06 10:48   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 04/14] selftests: dpll: add pin enumeration and property tests Petr Oros
2026-11-06 10:48   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 05/14] dpll: dpllsim: Add support to set input pin state Petr Oros
2026-11-06 10:41   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 06/14] selftests: dpll: add pin state tests Petr Oros
2026-11-06 10:49   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 07/14] dpll: dpllsim: Add support to get/set priority on input pins Petr Oros
2026-11-06 10:49   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 08/14] selftests: dpll: add pin priority test Petr Oros
2026-11-06 10:50   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 09/14] dpll: dpllsim: Add support to set frequency on pins Petr Oros
2026-11-06 10:50   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 10/14] selftests: dpll: add pin frequency test Petr Oros
2026-11-06 10:51   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 11/14] dpll: dpllsim: Add support to adjust phase Petr Oros
2026-11-06 10:51   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 12/14] selftests: dpll: add pin phase adjust test Petr Oros
2026-11-06 10:51   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 13/14] selftests: dpll: add concurrency stress tests Petr Oros
2026-11-06 10:52   ` Ivan Vecera
2026-11-05 15:18 ` [PATCH net-next v2 14/14] selftests: dpll: add reference leak tests Petr Oros
2026-11-06 10:47   ` Ivan Vecera
2026-11-06 12:27 ` [PATCH net-next v2 00/14] dpll: add a simulated DPLL device driver Jiri Pirko

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox