* [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver
@ 2026-11-12 13:03 Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 01/14] dpll: dpllsim: Add " Petr Oros
` (13 more replies)
0 siblings, 14 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 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.
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/dpll and run
with "make -C tools/testing/selftests TARGETS=drivers/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, and DPLL_DRIVER=<module name> picks the
first device of that driver, dpllsim for a simulated one. The whole
suite was run through the kselftest runner in a KVM guest and on an ice
E810 host with CONFIG_KASAN, CONFIG_PROVE_LOCKING and
CONFIG_DPLL_REFCNT_TRACKER enabled, against the simulator and, with
DPLL_DRIVER=ice, 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.
---
v4:
- address the findings of the netdev AI review of v3
- dpllsim: keep the priority and the frequency set over netlink apart
from the configured values, so that every live transition starts from
what configfs holds and the attributes keep showing it
- dpllsim: document the pin limit, the frequencies per pin type, the
errors of the live write and what happens to values set over netlink;
correct the comments on what the subsystem mutex covers
- selftests: tell a failed device or pin dump from a system without
devices or pins, and fail instead of skipping when the simulated
device cannot be created
- selftests: pick the spare input pin by all its parent devices, bail
out before touching a pin whose current priority, frequency or phase
adjust could not be written back, restore the reference after the
test pin in manual mode, do not let a refusal hide an earlier failure
- selftests: signal only workers that are still children of the script,
never delete a device named by the environment or one that existed
before, record the mounts before mounting
- selftests: keep the files of freed objects out of the reference
tracker snapshot, fail when the tracker is built in but records
nothing, skip the final leak check when a device was left behind
- selftests: log deferred restores that fail, tell failed pin queries
from missing attributes in the remaining places, fail a stress worker
that a signal interrupted, enable CONFIG_IKCONFIG_PROC for the
tracker probe
- drop the claim that configfs implies no stable ABI
- add the Reviewed-by given to patches 2, 5, 6 and 14 of v3 (Ivan
Vecera) and keep the other tags, except on patches 7 and 9, where
the behaviour of the driver changed and which ask for a new look
v3:
- move the selftests from tools/testing/selftests/drivers/net/dpll to
tools/testing/selftests/drivers/dpll, with the shared library as a
TEST_FILES entry of that directory, so that the tests sit where the
drivers do
- let dpllsim refuse pin states that are not valid in the device mode:
connected is refused in automatic mode, which the simulator runs in,
as Documentation/driver-api/dpll.rst reserves it for manual mode
(Ivan Vecera)
- make the pin state test follow the mode of the device under test and
check on the simulator that connected is refused in automatic mode;
cycle the stress tests through the states of automatic mode only
- add DPLL_DRIVER to pick the device of a given driver, or the simulator
with DPLL_DRIVER=dpllsim, on a system that has hardware DPLL devices
- describe what the leak check waits for instead of a concurrent dump
holding references, which nothing does (Ivan Vecera)
- carry the Reviewed-by and the set Acked-by of v2 over to the patches
whose code did not change or only moved; patches 2, 5 and 6 changed
and ask for a new look
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())
v3: https://lore.kernel.org/all/20261109093523.931201-1-poros@redhat.com/
v2: https://lore.kernel.org/all/20261105151852.1844305-1-poros@redhat.com/
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 | 95 ++
Documentation/driver-api/index.rst | 1 +
MAINTAINERS | 8 +
drivers/dpll/Kconfig | 1 +
drivers/dpll/Makefile | 1 +
drivers/dpll/dpllsim/Kconfig | 16 +
drivers/dpll/dpllsim/Makefile | 4 +
drivers/dpll/dpllsim/core.c | 480 +++++++++
drivers/dpll/dpllsim/dpll.c | 500 +++++++++
drivers/dpll/dpllsim/dpllsim.h | 101 ++
tools/testing/selftests/Makefile | 1 +
tools/testing/selftests/drivers/dpll/Makefile | 17 +
tools/testing/selftests/drivers/dpll/config | 8 +
.../selftests/drivers/dpll/dpll_basic.sh | 242 +++++
.../selftests/drivers/dpll/dpll_features.sh | 605 +++++++++++
.../selftests/drivers/dpll/dpll_lib.sh | 989 ++++++++++++++++++
.../selftests/drivers/dpll/dpll_reftrack.sh | 190 ++++
.../selftests/drivers/dpll/dpll_stress.sh | 610 +++++++++++
tools/testing/selftests/drivers/dpll/settings | 1 +
19 files changed, 3870 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/dpll/Makefile
create mode 100644 tools/testing/selftests/drivers/dpll/config
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_basic.sh
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_features.sh
create mode 100644 tools/testing/selftests/drivers/dpll/dpll_lib.sh
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_reftrack.sh
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_stress.sh
create mode 100644 tools/testing/selftests/drivers/dpll/settings
--
2.56.0
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH net-next v4 01/14] dpll: dpllsim: Add simulated DPLL device driver
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
` (12 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, 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
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.
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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- drop the claim that configfs implies no stable ABI from the
changelog, no code change
v3: https://lore.kernel.org/all/20261109093523.931201-2-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 fe23913b1e4329..0c8c36e90537f4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8099,6 +8099,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] 15+ messages in thread
* [PATCH net-next v4 02/14] selftests: dpll: add test library and device lifecycle tests
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 01/14] dpll: dpllsim: Add " Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 03/14] dpll: dpllsim: Add configurable pins Petr Oros
` (11 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, 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
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.
That includes the device dump the device selection reads. A dump that
fails is a failure, not a system without a device, unless the simulator
is missing as well, which is what a kernel without the DPLL subsystem
looks like and a skip. The library loads the dpllsim module and mounts
configfs when nothing else did, and the exit trap of each script undoes
the mount, 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.
The module stays loaded.
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, the
first device of the driver named by DPLL_DRIVER, or the first device
that is not a dpllsim one, and creates a dpllsim device when there is
none or when DPLL_DRIVER asks for dpllsim, so the same script serves
the simulator and real drivers and picks one of several drivers on a
host. A simulated device that cannot be created, registered or found on
netlink is a failure, only a missing module is a skip. 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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- tell a failed device dump from a system without a device, the dump
is a failure unless the simulator is missing as well
- fail instead of skipping when the simulated device cannot be created,
registered or found on netlink
- initialize DPLLSIM_CREATED_NAME, so that a value inherited from the
environment cannot make a cleanup handler delete a device, and never
record a directory that existed before as a device of the run
- record the configfs mount before mounting, so that a signal handled
right after the mount still unmounts it
- correct the comments on the side effects of the library and on how it
is used, drop the shellcheck directive of a function without
arguments
- add the Reviewed-by given to v3 (Ivan Vecera)
v3:
- move the selftests to tools/testing/selftests/drivers/dpll, next to
where the drivers live, with the library in that directory as a
TEST_FILES entry; the TARGETS and MAINTAINERS entries follow
- add DPLL_DRIVER, which picks the first device of the named driver
or, as dpllsim, a created simulated device on a system that has
hardware DPLL devices
- the Reviewed-by and Acked-by of v2 are not carried over, the
device selection changed
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)
v3: https://lore.kernel.org/all/20261109093523.931201-3-poros@redhat.com/
v2: https://lore.kernel.org/all/20261105151852.1844305-3-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-3-poros@redhat.com/
---
MAINTAINERS | 1 +
tools/testing/selftests/Makefile | 1 +
tools/testing/selftests/drivers/dpll/Makefile | 15 +
tools/testing/selftests/drivers/dpll/config | 3 +
.../selftests/drivers/dpll/dpll_basic.sh | 98 +++++
.../selftests/drivers/dpll/dpll_lib.sh | 381 ++++++++++++++++++
.../selftests/drivers/dpll/dpll_stress.sh | 164 ++++++++
tools/testing/selftests/drivers/dpll/settings | 1 +
8 files changed, 664 insertions(+)
create mode 100644 tools/testing/selftests/drivers/dpll/Makefile
create mode 100644 tools/testing/selftests/drivers/dpll/config
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_basic.sh
create mode 100644 tools/testing/selftests/drivers/dpll/dpll_lib.sh
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_stress.sh
create mode 100644 tools/testing/selftests/drivers/dpll/settings
diff --git a/MAINTAINERS b/MAINTAINERS
index 0c8c36e90537f4..c2998bb1d136af 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8105,6 +8105,7 @@ L: netdev@vger.kernel.org
S: Maintained
F: Documentation/driver-api/dpllsim.rst
F: drivers/dpll/dpllsim/
+F: tools/testing/selftests/drivers/dpll/
DPLL SUBSYSTEM
M: Vadim Fedorenko <vadim.fedorenko@linux.dev>
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 07c3719f8b04bf..9f07446a4e8c51 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -20,6 +20,7 @@ TARGETS += devices/error_logs
TARGETS += devices/probe
TARGETS += dmabuf-heaps
TARGETS += drivers/dma-buf
+TARGETS += drivers/dpll
TARGETS += drivers/ntsync
TARGETS += drivers/s390x/uvdevice
TARGETS += drivers/net
diff --git a/tools/testing/selftests/drivers/dpll/Makefile b/tools/testing/selftests/drivers/dpll/Makefile
new file mode 100644
index 00000000000000..7e839ae71cd22f
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/Makefile
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_INCLUDES := \
+ $(wildcard ../../net/lib/sh/*.sh) \
+ ../../net/lib.sh \
+# end of TEST_INCLUDES
+
+TEST_FILES := dpll_lib.sh
+
+TEST_PROGS := \
+ dpll_basic.sh \
+ dpll_stress.sh \
+# end of TEST_PROGS
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/drivers/dpll/config b/tools/testing/selftests/drivers/dpll/config
new file mode 100644
index 00000000000000..e2c90b6970bd43
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/config
@@ -0,0 +1,3 @@
+CONFIG_CONFIGFS_FS=y
+CONFIG_DPLL=y
+CONFIG_DPLLSIM=m
diff --git a/tools/testing/selftests/drivers/dpll/dpll_basic.sh b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
new file mode 100755
index 00000000000000..2f9a4370088a79
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
@@ -0,0 +1,98 @@
+#!/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. DPLL_DRIVER names the driver to take the
+# device from, dpllsim for a created one although hardware is present.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../net/lib.sh"
+source "$SCRIPTDIR/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
+
+CREATED_DEVICE=""
+# The device selection and 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
+
+dpll_lib_init
+
+# With the module there, a device that cannot be created, registered or
+# found on netlink is a failure of what the test covers, not a skip.
+if [ -z "$DPLL_DEV_ID" ]; then
+ dpllsim_check_module
+ if ! dpllsim_setup_standard_device; then
+ echo "FAIL: cannot create dpllsim device"
+ exit "$ksft_fail"
+ 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/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
new file mode 100644
index 00000000000000..9fe7f2f62dd460
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -0,0 +1,381 @@
+#!/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 only defines functions and variables. A
+# script calls dpll_lib_check_prereqs and dpll_lib_lock first, and
+# dpll_lib_init when it can run against an existing device. net/lib.sh
+# must be sourced first, it provides ksft_skip, ksft_fail, 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. Otherwise DPLL_DRIVER names the driver whose
+# first device is taken, by the module name the device reports, dpllsim
+# standing for a simulated device the script creates, and a system
+# without a device of that driver is a skip. With neither set the first
+# device that is not a dpllsim one is taken, 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. A device dump
+# that fails must not be taken for a system without a device, see
+# __dpll_lib_dump_failed, which may mount configfs, so the exit trap of
+# the script has to be in place before the call.
+dpll_lib_init()
+{
+ local out
+
+ 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
+ elif [ "${DPLL_DRIVER:-}" = "dpllsim" ]; then
+ DPLL_DEV_ID=""
+ elif [ -n "${DPLL_DRIVER:-}" ]; then
+ out=$(dpll device show -j 2>/dev/null) || __dpll_lib_dump_failed
+ DPLL_DEV_ID=$(jq -r --arg mod "$DPLL_DRIVER" '[.device[]? |
+ select(.["module-name"] == $mod)
+ ][0].id // empty' <<< "$out")
+ if [ -z "$DPLL_DEV_ID" ]; then
+ echo "SKIP: no DPLL device of driver \"$DPLL_DRIVER\""
+ exit "$ksft_skip"
+ fi
+ else
+ out=$(dpll device show -j 2>/dev/null) || __dpll_lib_dump_failed
+ DPLL_DEV_ID=$(jq -r '[.device[]? |
+ select(.["module-name"] != "dpllsim")
+ ][0].id // empty' <<< "$out")
+ fi
+
+ if [ -n "$DPLL_DEV_ID" ] &&
+ ! DPLL_MODULE_NAME=$(dpll_device_get_module "$DPLL_DEV_ID"); then
+ echo "FAIL: cannot query DPLL device $DPLL_DEV_ID"
+ exit "$ksft_fail"
+ fi
+}
+
+# The device dump failed. On a kernel without the DPLL subsystem it
+# fails too, and such a kernel has no simulator either, which is the
+# skip of the module check. With the simulator present the subsystem is
+# there and the failed dump is a failure of what the tests cover.
+__dpll_lib_dump_failed()
+{
+ dpllsim_check_module
+ echo "FAIL: cannot dump DPLL devices"
+ exit "$ksft_fail"
+}
+
+##############################################################################
+# 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 loads the dpllsim module and mounts configfs when
+# nothing else did. Scripts call dpll_lib_cleanup last from their exit
+# trap to undo the mount, the module stays loaded. The only other trace
+# the library leaves outside the dpllsim directory is DPLL_RUN_DIR with
+# the lock file.
+
+DPLLSIM_MOUNTED_CONFIGFS=0
+# Name of the device dpllsim_create_device created last. Set here, so
+# that a value inherited from the environment is never taken for a
+# device of this run by a cleanup handler.
+DPLLSIM_CREATED_NAME=""
+
+dpllsim_available()
+{
+ [ -d "$DPLLSIM_CFG" ] && return 0
+
+ modprobe dpllsim &> /dev/null
+ if ! mountpoint -q "$CONFIGFS_DIR"; then
+ # Recorded before the mount, so that a cleanup run by a
+ # signal right after it knows about the mount.
+ DPLLSIM_MOUNTED_CONFIGFS=1
+ mount -t configfs none "$CONFIGFS_DIR" &> /dev/null ||
+ DPLLSIM_MOUNTED_CONFIGFS=0
+ 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
+ # The mount recorded up front may have failed.
+ mountpoint -q "$CONFIGFS_DIR" || 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"
+ 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"
+
+ # A directory of that name was left behind by something else and
+ # must never be recorded as a device of this run.
+ [ -e "$dev_path" ] && return 1
+
+ # 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.
+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.
+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
+}
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
new file mode 100755
index 00000000000000..01d57ab0172b0f
--- /dev/null
+++ b/tools/testing/selftests/drivers/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/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/dpll/settings b/tools/testing/selftests/drivers/dpll/settings
new file mode 100644
index 00000000000000..a62d2fa1275c6b
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/settings
@@ -0,0 +1 @@
+timeout=600
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v4 03/14] dpll: dpllsim: Add configurable pins
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 01/14] dpll: dpllsim: Add " Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 04/14] selftests: dpll: add pin enumeration and property tests Petr Oros
` (10 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, 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
Pins are the unit of most DPLL netlink interaction, so let a device
instance carry a set of up to 128 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. configfs calls make_item
and drop_item under the subsystem mutex but links and unlinks the pin
directory outside of it, so a live transition racing with a pin mkdir
or rmdir can find a pin it cannot hold and fails with ENOENT then.
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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- document the limit of 128 pins, that pins cannot be added to a live
instance, the frequencies each pin type supports and the errors of
the live write
- correct the comments on what the subsystem mutex covers, a live write
racing with a pin mkdir or rmdir fails with ENOENT
v3: https://lore.kernel.org/all/20261109093523.931201-4-poros@redhat.com/
---
Documentation/driver-api/dpllsim.rst | 57 +++++-
drivers/dpll/dpllsim/core.c | 234 +++++++++++++++++++++-
drivers/dpll/dpllsim/dpll.c | 280 ++++++++++++++++++++++++++-
drivers/dpll/dpllsim/dpllsim.h | 32 +++
4 files changed, 591 insertions(+), 12 deletions(-)
diff --git a/Documentation/driver-api/dpllsim.rst b/Documentation/driver-api/dpllsim.rst
index 86a5d523534055..a0e5ea44d2c6d3 100644
--- a/Documentation/driver-api/dpllsim.rst
+++ b/Documentation/driver-api/dpllsim.rst
@@ -12,10 +12,13 @@ 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 can neither be added nor removed. An
+instance holds up to 128 pins, creating one more fails with ``ENOSPC``.
Device directory ``/sys/kernel/config/dpllsim/<dev>/``:
@@ -25,17 +28,57 @@ 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.
+
+The frequencies a pin type supports:
+
+================== ==========================================================
+``gnss`` 1 Hz
+``int-oscillator`` 25 MHz
+``synce-eth-port`` 1.544 MHz, 2.048 MHz, 10 MHz and 25 MHz
+``mux`` same as ``synce-eth-port``
+``ext`` 1 Hz, 10 MHz, 19.44 MHz, 25 MHz, 77.76 MHz, 125 MHz and
+ 156.25 MHz
+================== ==========================================================
+
+The pin configuration is checked when 1 is written to ``live``. The
+write fails with ``EINVAL`` when a pin has a frequency its type does not
+support or when a ``mux`` pin is an output, and with ``ENOENT`` when a
+pin directory is being created or removed at the same time.
+
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..21c56aa4ecf5ac 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,189 @@ 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;
+}
+
+/*
+ * make_item and drop_item run under the subsystem mutex like the live
+ * transitions, which keeps pin_list consistent. configfs links the
+ * directory of a new pin only after make_item returned and unlinks the
+ * one of a removed pin before it calls drop_item, both without the
+ * mutex. A live transition in between finds a pin on the list that
+ * configfs_depend_item_unlocked() does not know and fails with -ENOENT.
+ */
+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 +267,41 @@ 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 keeps the make_item and drop_item callbacks of the
+ * pins and the live transitions of the other instances out for the whole
+ * deploy sequence, see dpllsim_pins_drop_item() for a pin mkdir or rmdir
+ * that is in flight around its callback.
*/
static ssize_t dpllsim_dev_live_store(struct config_item *item,
const char *page, size_t count)
@@ -104,6 +321,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 +330,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 +378,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] 15+ messages in thread
* [PATCH net-next v4 04/14] selftests: dpll: add pin enumeration and property tests
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (2 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 03/14] dpll: dpllsim: Add configurable pins Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 05/14] dpll: dpllsim: Add support to set input pin state Petr Oros
` (9 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, 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
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.
The helpers that print a list of pin ids are read through
dpll_pin_ids_read, which keeps their status where a process
substitution or a pipeline would drop it, so that a pin dump that fails
is reported as a failure rather than as a device without pins.
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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- read the pin lists through dpll_pin_ids_read, which keeps the status
of the dump, so that a failed dump is a failure and not a device
without pins
- correct the comment on pins without a parent-device nest
v3:
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v2:
- quote the ksft_skip and ret expansions (netdev CI shellcheck)
v3: https://lore.kernel.org/all/20261109093523.931201-5-poros@redhat.com/
v2: https://lore.kernel.org/all/20261105151852.1844305-5-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-5-poros@redhat.com/
---
.../selftests/drivers/dpll/dpll_basic.sh | 164 +++++++++++++-
.../selftests/drivers/dpll/dpll_lib.sh | 205 +++++++++++++++++-
.../selftests/drivers/dpll/dpll_stress.sh | 46 +++-
3 files changed, 390 insertions(+), 25 deletions(-)
diff --git a/tools/testing/selftests/drivers/dpll/dpll_basic.sh b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
index 2f9a4370088a79..c6b223d1368c4a 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_basic.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
@@ -1,11 +1,12 @@
#!/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. DPLL_DRIVER names the driver to take the
-# device from, dpllsim for a created one although hardware is present.
+# 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. DPLL_DRIVER names
+# the driver to take the device from, dpllsim for a created one although
+# hardware is present.
#
# Copyright (c) 2026 Red Hat, Inc.
@@ -16,8 +17,29 @@ source "$SCRIPTDIR/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
@@ -43,6 +65,129 @@ test_device_properties()
log_test "DPLL device properties"
}
+test_pin_enumeration()
+{
+ local pin_ids
+
+ RET=0
+
+ # A dump that failed is not a device without pins.
+ if ! dpll_pin_ids_read pin_ids dpll_pin_get_ids "$DPLL_DEV_ID"; then
+ check_err 1 "pin dump failed"
+ log_test "DPLL pin enumeration"
+ return
+ fi
+ 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
+
+ # A pin registered on a parent pin has no parent-device nest, 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
+
+ if ! dpll_pin_ids_read pin_ids dpll_pin_get_ids "$DPLL_DEV_ID"; then
+ check_err 1 "pin dump failed"
+ log_test "DPLL pin properties"
+ return
+ fi
+ 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_ids pin_id state
+
+ RET=0
+
+ if ! dpll_pin_ids_read pin_ids dpll_pin_find_by_direction \
+ "$DPLL_DEV_ID" input; then
+ check_err 1 "pin dump failed"
+ log_test "DPLL pin state"
+ return
+ fi
+ pin_id=${pin_ids[0]:-}
+ 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()
@@ -50,9 +195,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 +213,6 @@ dpll_lib_check_prereqs
# lock in the library.
dpll_lib_lock
-CREATED_DEVICE=""
# The device selection and 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
@@ -86,8 +230,8 @@ if [ -z "$DPLL_DEV_ID" ]; then
echo "FAIL: cannot create dpllsim device"
exit "$ksft_fail"
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/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 9fe7f2f62dd460..2da8cbe8a883ef 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -46,6 +46,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
@@ -142,14 +146,39 @@ __dpll_lib_dump_failed()
exit "$ksft_fail"
}
+# 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()
{
@@ -231,12 +260,126 @@ 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"
+}
+
+# Run a helper that prints one pin id per line, dpll_pin_get_ids or
+# dpll_pin_find_by_direction with its arguments, and store the ids in the
+# array named by $1. Return 1 when the helper failed, the status a
+# process substitution or a pipeline would drop, so that a caller can
+# tell a failed dump from a device without such pins.
+# $1: array name, $2...: helper and its arguments
+dpll_pin_ids_read()
+{
+ local -n __dpll_ids=$1; shift
+ local out
+
+ __dpll_ids=()
+ out=$("$@") || return 1
+ [ -z "$out" ] || mapfile -t __dpll_ids <<< "$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 loads the dpllsim module and mounts configfs when
@@ -324,6 +467,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"
@@ -334,17 +499,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"
@@ -357,8 +527,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
dpllsim_setup_standard_device()
{
local clock_id name
@@ -368,6 +542,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
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index 01d57ab0172b0f..b21325d8368fb2 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/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)"
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v4 05/14] dpll: dpllsim: Add support to set input pin state
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (3 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 04/14] selftests: dpll: add pin enumeration and property tests Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 06/14] selftests: dpll: add pin state tests Petr Oros
` (8 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, 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
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 the states valid for the device mode. The core checks the
capability only, the states a mode allows are up to the driver: in
automatic mode a pin is selectable or disconnected, in manual mode
connected or disconnected, as Documentation/driver-api/dpll.rst
describes them. The device reports automatic mode, so connected is
refused with -EINVAL and an extack message. The check is written for
both modes already, so that manual mode has nothing to add to it when
it arrives. 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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- no change, add the Reviewed-by given to v3 (Ivan Vecera)
v3:
- refuse pin states that are not valid in the device mode, so that
connected is refused in automatic mode, the mode the device reports,
with an extack message (Ivan Vecera)
- the Acked-by of v2 is not carried over, the callback changed
v3: https://lore.kernel.org/all/20261109093523.931201-6-poros@redhat.com/
v2: https://lore.kernel.org/all/20261105151852.1844305-6-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-6-poros@redhat.com/
---
drivers/dpll/dpllsim/dpll.c | 42 +++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index a1d5921fb8d017..794020d0c112e8 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -104,10 +104,46 @@ static int dpllsim_pin_state_get(const struct dpll_pin *pin, void *pin_priv,
return 0;
}
+/*
+ * The states a user may request depend on the device mode: connected and
+ * disconnected select and deselect the reference in manual mode, selectable
+ * and disconnected offer a pin to or withdraw it from the automatic
+ * selection. The core checks the state-can-change capability only.
+ */
+static bool dpllsim_pin_state_valid(const struct dpllsim_dev *sim,
+ enum dpll_pin_state state)
+{
+ if (state == DPLL_PIN_STATE_DISCONNECTED)
+ return true;
+ if (sim->mode == DPLL_MODE_MANUAL)
+ return state == DPLL_PIN_STATE_CONNECTED;
+ return state == DPLL_PIN_STATE_SELECTABLE;
+}
+
+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;
+ struct dpllsim_dev *sim = sim_pin->dev;
+
+ guard(mutex)(&sim->lock);
+ if (!dpllsim_pin_state_valid(sim, state)) {
+ NL_SET_ERR_MSG_FMT(extack, "Pin state not valid in %s mode",
+ sim->mode == DPLL_MODE_MANUAL ?
+ "manual" : "automatic");
+ return -EINVAL;
+ }
+ 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 +185,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] 15+ messages in thread
* [PATCH net-next v4 06/14] selftests: dpll: add pin state tests
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (4 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 05/14] dpll: dpllsim: Add support to set input pin state Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 07/14] dpll: dpllsim: Add support to get/set priority on input pins Petr Oros
` (7 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, 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
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 or the first device of the driver named by
DPLL_DRIVER, and picks the pin to reconfigure up front, so that no test
touches the reference the device is locked to. The pin is an input that
is disconnected or selectable on the device and that none of its parent
devices reports as connected or, where the tool prints the operational
state, as active, since a pin can be registered on several devices.
When the first input pin and another one both qualify the other one is
taken, drivers tend to list their primary reference first. On hardware
a spare pin with a valid signal can still be selected while a test
reconfigures it, which the header of the script points out.
The first test moves that pin through the states of the device mode,
as Documentation/driver-api/dpll.rst defines them, reads the state back
after each step and restores the original state at the end. In
automatic mode these are disconnected and selectable, and on the
device the script created the test then checks that connected is
refused. Hardware drivers are not held to that, ice accepts connected
in automatic mode, and a pin with the state-connected-override
capability would take over the reference. In manual mode connected and
disconnected
are the states, connecting the pin makes it the reference, so the test
finds the pin connected before, connects it back at the end and checks
that it is the reference again.
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 of its
mode. A refusal that follows a failed readback leaves the test failed,
dpll_log_test_skip_or_fail no longer turns a recorded failure into a
skip. The restores are deferred right after the original states are
read, the reconnect of the reference first, since deferred commands run
last in first out and the test pin has to be disconnected again before
the reference is connected back. 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. defer drops the status of what it runs, so the restores go
through dpll_restore, which logs one that fails. A pin that cannot be
queried is a failure, not a pin that is in use or lacks a capability.
The stress script gains a test that floods one input pin of a fresh
device with changes between the states of automatic mode and ends on
selectable, the state a fresh pin is not in, so that the readback after
the flood catches a setter that stores nothing. 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,
the device mode getter, 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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- defer the reconnect of the reference before the restore of the test
pin, deferred commands run last in first out
- do not let a refusal turn a test that already failed into a skip
- pick the spare pin by all its parent devices and by the operational
state where the tool prints it, and leave it alone when the automatic
selection took it in the meantime
- accept connected when a pin was set selectable in automatic mode,
the device may pick the pin it was offered as its reference, also
when the restore makes it selectable again
- end the state flood on selectable, so that the readback catches a
setter that stores nothing
- fail when the pin dump fails or the simulated device cannot be created
- say in the header what the tests change on a device the user provided
- log a deferred restore that fails, defer drops the status of what it
runs
- tell a pin that could not be queried from one that is in use or
lacks a capability, and log the reason of a refusal that follows a
failure
- take the number of state changes of the flood from one variable
- add the Reviewed-by given to v3 (Ivan Vecera)
v3:
- follow the mode of the device under test: disconnected and
selectable in automatic mode, where the simulator is checked to
refuse connected, connected and disconnected in manual mode, where
the pin connected before is connected back at the end
- cycle the state flooding test through the states of automatic mode
- add the device mode getter to the library
- honor DPLL_DRIVER like dpll_basic.sh does
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
- the Reviewed-by and Acked-by of v2 are not carried over, the
state test changed
v2:
- quote the ksft_skip expansion (netdev CI shellcheck)
v3: https://lore.kernel.org/all/20261109093523.931201-7-poros@redhat.com/
v2: https://lore.kernel.org/all/20261105151852.1844305-7-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-7-poros@redhat.com/
---
tools/testing/selftests/drivers/dpll/Makefile | 1 +
.../selftests/drivers/dpll/dpll_features.sh | 339 ++++++++++++++++++
.../selftests/drivers/dpll/dpll_lib.sh | 102 +++++-
.../selftests/drivers/dpll/dpll_stress.sh | 73 ++++
4 files changed, 513 insertions(+), 2 deletions(-)
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_features.sh
diff --git a/tools/testing/selftests/drivers/dpll/Makefile b/tools/testing/selftests/drivers/dpll/Makefile
index 7e839ae71cd22f..1349d078fa2660 100644
--- a/tools/testing/selftests/drivers/dpll/Makefile
+++ b/tools/testing/selftests/drivers/dpll/Makefile
@@ -9,6 +9,7 @@ TEST_FILES := dpll_lib.sh
TEST_PROGS := \
dpll_basic.sh \
+ dpll_features.sh \
dpll_stress.sh \
# end of TEST_PROGS
diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
new file mode 100755
index 00000000000000..0a00a18bded2c5
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
@@ -0,0 +1,339 @@
+#!/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 or DPLL_DRIVER to run against an existing
+# device instead, the checks that hold for the simulator alone then run
+# only when the script created the device, which DPLL_DRIVER=dpllsim
+# asks for.
+#
+# The tests reconfigure a spare input pin of the device and restore it
+# afterwards, see pick_tune_pin. A pin the device uses as its reference
+# is never picked, but on hardware a spare pin that carries a valid
+# signal can still be selected while a test reconfigures it, so do not
+# point the script at a device whose reference must not be disturbed.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../net/lib.sh"
+source "$SCRIPTDIR/dpll_lib.sh"
+
+# The input pins of the device under test, read once it is known.
+input_pins=()
+
+ALL_TESTS="
+ test_pin_state_transitions
+"
+
+##############################################################################
+# Pin-level feature tests
+
+# Find the input pin connected on the device, the reference of a device in
+# manual mode. Prints nothing when there is none.
+find_connected_pin()
+{
+ local pin_id
+
+ for pin_id in "${input_pins[@]}"; do
+ if [ "$(dpll_pin_get_state "$DPLL_DEV_ID" "$pin_id")" = \
+ connected ]; then
+ echo "$pin_id"
+ return
+ fi
+ done
+}
+
+# Return 0 when PIN_TUNE_ID advertises the capability. Otherwise log the
+# test, as failed when the pin could not be queried and as skipped or
+# failed with the given reason when it lacks the capability, and return
+# 1.
+# $1: capability, $2: test name, $3: reason
+pin_tune_capable()
+{
+ dpll_pin_has_capability "$PIN_TUNE_ID" "$1"
+ case $? in
+ 0)
+ return 0
+ ;;
+ 1)
+ dpll_log_test_skip_or_fail "$2" "$3"
+ ;;
+ *)
+ check_err 1 "pin $PIN_TUNE_ID query failed"
+ log_test "$2"
+ ;;
+ esac
+ return 1
+}
+
+test_pin_state_transitions()
+{
+ local mode before state after reason ref_pin
+ local states
+
+ 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
+ pin_tune_capable state-can-change "DPLL pin state transitions" \
+ "state cannot change" || return
+
+ # The states a user may request depend on the device mode, see
+ # Documentation/driver-api/dpll.rst: connected and disconnected
+ # select and deselect the reference in manual mode, selectable and
+ # disconnected offer a pin to the automatic selection or withdraw
+ # it. The simulator runs in automatic mode.
+ mode=$(dpll_device_get_mode "$DPLL_DEV_ID")
+ case "$mode" in
+ automatic)
+ states="disconnected selectable"
+ ;;
+ manual)
+ states="connected disconnected"
+ ;;
+ *)
+ dpll_log_test_skip_or_fail "DPLL pin state transitions" \
+ "device mode '$mode' not handled"
+ return
+ ;;
+ esac
+
+ 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
+ # The automatic selection may have taken the pin since it was
+ # picked, it is the reference then and not to be touched.
+ if [ "$mode" = automatic ] && [ "$before" = connected ]; then
+ dpll_log_test_skip_or_fail "DPLL pin state transitions" \
+ "spare input pin became the reference"
+ return
+ fi
+
+ # In manual mode connecting the pin makes it the reference and
+ # takes that over from the pin connected so far, so that one is
+ # connected back at the end. The deferred commands run last in
+ # first out, so the reconnect is deferred first and runs after the
+ # restore deferred below has disconnected the test pin again. In
+ # automatic mode the pin is not connected, see how PIN_TUNE_ID is
+ # picked, and the test never connects it, the device keeps the
+ # reference it locked to.
+ ref_pin=""
+ if [ "$mode" = manual ]; then
+ ref_pin=$(find_connected_pin)
+ if [ -n "$ref_pin" ]; then
+ defer dpll_restore dpll_pin_set_state "$DPLL_DEV_ID" \
+ "$ref_pin" connected
+ fi
+ fi
+ # Restore the original state even when the test aborts half way.
+ defer dpll_restore dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" \
+ "$before"
+
+ # 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
+ # of its mode. The test ends there, whichever set was refused, a
+ # state that was not set has nothing to read back and the deferred
+ # restores put the pins back. A refusal after a readback failed
+ # does not turn the test into a skip, see
+ # dpll_log_test_skip_or_fail. A refused restore is treated the same
+ # way when nothing failed before it, afterwards it is one more
+ # failure.
+ for state in $states; 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
+ # A device in automatic mode may pick the pin it was
+ # offered as its reference, the pin reports connected
+ # then.
+ after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ "$after" != "$state" ] &&
+ [ "$mode/$state/$after" != automatic/selectable/connected ]
+ then
+ check_err 1 "pin not $state, state '$after'"
+ fi
+ done
+ state=$after
+
+ # Connected is not a state of automatic mode and the simulator
+ # refuses it. Hardware drivers are not held to that, ice for one
+ # accepts it there, and a pin with the state-connected-override
+ # capability would take over the reference, so the check runs on
+ # the device the script created only.
+ if [ "$mode" = automatic ] && [ -n "$DPLL_CREATED_DEVICE" ]; then
+ if dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" \
+ connected; then
+ check_err 1 "connected accepted in automatic mode"
+ fi
+ after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ "$after" != "$state" ]; then
+ check_err 1 "refused state changed the pin to '$after'"
+ fi
+ fi
+
+ 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
+ # The automatic selection may pick a pin that is selectable again,
+ # like it may in the loop above.
+ after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ "$after" != "$before" ] &&
+ [ "$mode/$before/$after" != automatic/selectable/connected ]; then
+ check_err 1 "pin state not restored, got '$after'"
+ fi
+
+ if [ -n "$ref_pin" ]; then
+ dpll_pin_set_state "$DPLL_DEV_ID" "$ref_pin" connected
+ check_err $? "cannot connect the reference pin $ref_pin back"
+ after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$ref_pin")
+ if [ "$after" != connected ]; then
+ check_err 1 "reference pin $ref_pin is '$after'"
+ fi
+ 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:-}" ] || [ -n "${DPLL_DRIVER:-}" ]; then
+ dpll_lib_init
+fi
+# With the module there, a device that cannot be created, registered or
+# found on netlink is a failure of what the tests cover, not a skip.
+if [ -z "${DPLL_DEV_ID:-}" ]; then
+ dpllsim_check_module
+ if ! dpllsim_setup_standard_device; then
+ echo "FAIL: cannot create dpllsim device"
+ exit "$ksft_fail"
+ 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)"
+
+# Return 0 when the pin is a spare input of the device: disconnected or
+# selectable on it and not reported as connected by any of its parent
+# devices nor, where the tool prints the operational state, as active.
+# A pin can be registered on several devices, and what a test changes
+# on it, the frequency for one, is shared by all of them. Return 1 when
+# the pin is not spare and 2 when it could not be queried.
+# $1: device id, $2: pin id
+pin_is_spare()
+{
+ local out
+
+ out=$(dpll pin show id "$2" -j 2>/dev/null) || return 2
+ jq -e --argjson dev "$1" '
+ (.pin[0]? // .) | [.["parent-device"][]?] |
+ any(.[]; .["parent-id"] == $dev and
+ (.state == "disconnected" or
+ .state == "selectable")) and
+ all(.[]; .state != "connected" and .operstate != "active")' \
+ <<< "$out" &> /dev/null || return 1
+}
+
+# PIN_TUNE_ID is the input pin the tests reconfigure. It must not be the
+# reference the device is locked to, so it is taken from the spare
+# inputs, see pin_is_spare. When the first dumped input pin and another
+# one both qualify the other one is taken, drivers tend to list their
+# primary reference first. Tests that need the pin skip when there is
+# none rather than reconfiguring the reference. A pin that cannot be
+# queried is a failure, not a pin that is in use.
+pick_tune_pin()
+{
+ local pin_id
+
+ PIN_TUNE_ID=""
+ for pin_id in "${input_pins[@]}"; do
+ pin_is_spare "$DPLL_DEV_ID" "$pin_id"
+ case $? in
+ 0)
+ ;;
+ 1)
+ continue
+ ;;
+ *)
+ echo "FAIL: cannot query pin $pin_id"
+ exit "$ksft_fail"
+ ;;
+ esac
+ PIN_TUNE_ID=$pin_id
+ [ "$pin_id" != "${input_pins[0]}" ] && return
+ done
+}
+
+# A dump that failed is not a device without input pins.
+if ! dpll_pin_ids_read input_pins dpll_pin_find_by_direction \
+ "$DPLL_DEV_ID" input; then
+ echo "FAIL: cannot dump the pins of DPLL device $DPLL_DEV_ID"
+ exit "$ksft_fail"
+fi
+pick_tune_pin
+
+tests_run
+
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 2da8cbe8a883ef..783db2a4dbc180 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -7,7 +7,7 @@
# script calls dpll_lib_check_prereqs and dpll_lib_lock first, and
# dpll_lib_init when it can run against an existing device. net/lib.sh
# must be sourced first, it provides ksft_skip, ksft_fail, busywait and
-# log_info.
+# log_info to the library and defer to the scripts.
#
# Copyright (c) 2026 Red Hat, Inc.
#
@@ -88,6 +88,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. Otherwise DPLL_DRIVER names the driver whose
# first device is taken, by the module name the device reports, dpllsim
@@ -155,7 +193,8 @@ DPLL_CREATED_DEVICE=""
# 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.
+# where it is a property of the hardware. A test that recorded a failure
+# before stays failed on either, a skip would hide that failure.
# $1: test name, $2: reason
dpll_log_test_skip_or_fail()
{
@@ -165,6 +204,11 @@ dpll_log_test_skip_or_fail()
if [ -n "$DPLL_CREATED_DEVICE" ]; then
check_err 1 "$reason on the device created by the test"
log_test "$name"
+ elif [ "$RET" -ne 0 ]; then
+ # The failed test reports the message of its first failure,
+ # so this reason is logged next to it.
+ log_test "$name"
+ log_info "$name: $reason"
else
log_test_skip "$name" "$reason"
fi
@@ -203,6 +247,11 @@ dpll_device_get_module()
__dpll_device_field "$1" '.["module-name"]'
}
+dpll_device_get_mode()
+{
+ __dpll_device_field "$1" '.mode'
+}
+
# 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
@@ -373,6 +422,55 @@ 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, ...), 1
+# when it does not and 2 when the pin could not be queried.
+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 2
+ jq -e --arg name "$name" \
+ '(.pin[0]? // .) | (.capabilities // []) | index($name) != null' \
+ <<< "$out" &> /dev/null || return 1
+}
+
+##############################################################################
+# 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=""
+
+# Run a setter that puts a pin back and log when it fails. The scripts
+# defer their restores through this, defer drops the status of what it
+# runs and a pin left changed would go unnoticed.
+# $1...: setter and its arguments
+dpll_restore()
+{
+ local reason
+
+ "$@" && return 0
+ reason=${DPLL_LAST_ERR%%$'\n'*}
+ log_info "restore failed: $*${reason:+, $reason}"
+}
+
+# $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
#
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index b21325d8368fb2..396d44c7bbe351 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
@@ -15,6 +15,7 @@ source "$SCRIPTDIR/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,77 @@ test_rapid_create_delete()
log_test "DPLL rapid create/delete ($STRESS_ITERATIONS iterations)"
}
+test_pin_state_flooding()
+{
+ local clock_id name dev_id pin_ids pin_id i last state fails=0
+ local changes=${STRESS_STATE_CHANGES:-50}
+ # The states of automatic mode, the mode the simulator runs in. A
+ # fresh pin is disconnected, so the flood ends on selectable, the
+ # last change takes states[0] whatever their number, and the
+ # readback after it catches a setter that stores nothing.
+ local states=(selectable disconnected)
+
+ 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
+ dpll_pin_ids_read pin_ids dpll_pin_find_by_direction \
+ "$dev_id" input
+ check_err $? "pin dump failed"
+ pin_id=${pin_ids[0]:-}
+ if [ -z "$pin_id" ]; then
+ check_err 1 "no input pin on stress device"
+ else
+ for i in $(seq 1 "$changes"); do
+ last=${states[(changes - i) % 2]}
+ dpll_pin_set_state "$dev_id" "$pin_id" \
+ "$last" ||
+ fails=$((fails + 1))
+ done
+ if [ "$fails" -gt 0 ]; then
+ check_err 1 \
+ "$fails of $changes state changes failed"
+ fi
+ # A device in automatic mode may pick the pin it
+ # was offered as its reference, the pin reports
+ # connected then.
+ state=$(dpll_pin_get_state "$dev_id" "$pin_id")
+ case "$state" in
+ selectable|connected)
+ ;;
+ *)
+ check_err 1 \
+ "state expected '$last', got '$state'"
+ ;;
+ esac
+ 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
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v4 07/14] dpll: dpllsim: Add support to get/set priority on input pins
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (5 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 06/14] selftests: dpll: add pin state tests Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 08/14] selftests: dpll: add pin priority test Petr Oros
` (6 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 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, the driver does not select a
reference by them. They are kept apart from the configured value, so
that every live transition starts from what configfs holds, like the
pin state does, and the attribute keeps showing what was configured.
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>
---
v4:
- keep the priority set over netlink apart from the configured one,
every live transition starts from the configured value and the
attribute keeps showing it
- document that, and that the driver only stores the priority
- the Reviewed-by and Acked-by of v3 are not carried over, the
behaviour of the driver changed
v3: https://lore.kernel.org/all/20261109093523.931201-8-poros@redhat.com/
---
Documentation/driver-api/dpllsim.rst | 15 +++++++--
drivers/dpll/dpllsim/core.c | 31 +++++++++++++++++++
drivers/dpll/dpllsim/dpll.c | 46 ++++++++++++++++++++++++++--
drivers/dpll/dpllsim/dpllsim.h | 6 ++++
4 files changed, 94 insertions(+), 4 deletions(-)
diff --git a/Documentation/driver-api/dpllsim.rst b/Documentation/driver-api/dpllsim.rst
index a0e5ea44d2c6d3..27cfc4b2c71518 100644
--- a/Documentation/driver-api/dpllsim.rst
+++ b/Documentation/driver-api/dpllsim.rst
@@ -37,6 +37,8 @@ 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`` Initial priority 0-255 of an input pin, output pins must
+ keep the default (default 128)
============= ===============================================================
The pin directory name becomes the board label of the pin. Pins are
@@ -55,8 +57,15 @@ The frequencies a pin type supports:
The pin configuration is checked when 1 is written to ``live``. The
write fails with ``EINVAL`` when a pin has a frequency its type does not
-support or when a ``mux`` pin is an output, and with ``ENOENT`` when a
-pin directory is being created or removed at the same time.
+support, when a ``mux`` pin is an output or when an output pin has a
+priority other than the default, and with ``ENOENT`` when a pin
+directory is being created or removed at the same time.
+
+Every live transition starts from the configured values. A priority set
+over netlink while the instance is live is reported back over netlink,
+it is not written to the ``prio`` attribute and is gone once the
+instance is taken down. The driver stores the priority, it does not
+select a reference by it.
Example
=======
@@ -70,8 +79,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 21c56aa4ecf5ac..fca16ec0976684 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->cfg_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->cfg_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->cfg_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 794020d0c112e8..9fdb5dbeba4400 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -139,11 +139,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 = {
@@ -188,7 +220,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;
}
@@ -214,10 +247,18 @@ 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->cfg_prio == DPLLSIM_DEFAULT_PIN_PRIO;
+}
+
static void dpllsim_pin_reset(struct dpllsim_pin *pin)
{
pin->ops = dpllsim_pin_select_ops(pin);
pin->state = DPLL_PIN_STATE_DISCONNECTED;
+ pin->prio = pin->cfg_prio;
}
/* Build sim->pins in registration order, the order the pins were created */
@@ -228,7 +269,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..54f4f1636f64e4 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;
@@ -25,6 +28,7 @@ struct dpllsim_pin {
enum dpll_pin_type type;
enum dpll_pin_direction direction;
u64 frequency;
+ u32 cfg_prio;
/* Resolved at live=1 */
int idx;
@@ -35,6 +39,8 @@ struct dpllsim_pin {
/* Simulated state, guarded by dev->lock */
enum dpll_pin_state state;
+ /* Starts from cfg_prio at live=1, changed by the netlink setter */
+ u32 prio;
};
struct dpllsim_dev {
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v4 08/14] selftests: dpll: add pin priority test
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (6 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 07/14] dpll: dpllsim: Add support to get/set priority on input pins Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 09/14] dpll: dpllsim: Add support to set frequency on pins Petr Oros
` (5 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, 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
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 value
the test tries. A current value the device does not take back could
not be restored, so the test writes it back once before it touches the
pin, which changes nothing, and ends there when that is refused. The
test skips when the dpll tool lacks the prio keyword of pin set, and
the restore is deferred right after that write so that an interrupted
run restores the value 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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- write the current priority back before the pin is touched and end
the test when the device refuses it, the pin could not be restored
- say that the simulator accepts every value the test tries, not every
priority, and drop "active reference" from the layout comment
- tell a failed capability query from a missing capability and log a
deferred restore that fails
v3:
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v3: https://lore.kernel.org/all/20261109093523.931201-9-poros@redhat.com/
v2: https://lore.kernel.org/all/20261105151852.1844305-9-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-9-poros@redhat.com/
---
.../selftests/drivers/dpll/dpll_features.sh | 74 +++++++++++++++++++
.../selftests/drivers/dpll/dpll_lib.sh | 32 ++++++--
.../selftests/drivers/dpll/dpll_stress.sh | 15 ++--
3 files changed, 109 insertions(+), 12 deletions(-)
diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
index 0a00a18bded2c5..f7340bd89602fe 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_features.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
@@ -27,6 +27,7 @@ input_pins=()
ALL_TESTS="
test_pin_state_transitions
+ test_pin_priority_get_set
"
##############################################################################
@@ -70,6 +71,79 @@ pin_tune_capable()
return 1
}
+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
+ pin_tune_capable priority-can-change "DPLL pin priority set/get" \
+ "priority cannot change" || return
+
+ 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
+ # A device may report a priority it does not accept back, one that
+ # marks an unused pin for instance, and the pin could not be
+ # restored then. Writing the current value changes nothing, so
+ # that is tried before the pin is touched.
+ if ! dpll_pin_set_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"; then
+ reason=${DPLL_LAST_ERR%%$'\n'*}
+ dpll_log_test_skip_or_fail "DPLL pin priority set/get" \
+ "cannot write the current prio back${reason:+, $reason}"
+ return
+ fi
+ defer dpll_restore 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 value
+ # tried here.
+ 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 mode before state after reason ref_pin
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 783db2a4dbc180..b8f4375a4f76a7 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -437,6 +437,11 @@ dpll_pin_has_capability()
<<< "$out" &> /dev/null || return 1
}
+dpll_pin_get_prio()
+{
+ __dpll_pin_dev_field "$1" "$2" '.prio'
+}
+
##############################################################################
# Pin setters
#
@@ -471,6 +476,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
#
@@ -567,7 +582,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
@@ -575,13 +592,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
@@ -628,8 +648,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
+# ext0: EXT, 10 MHz, input, prio 1
# out0: EXT, 10 MHz, output
dpllsim_setup_standard_device()
{
@@ -640,8 +660,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
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index 396d44c7bbe351..1745643955da16 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/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
@@ -221,8 +224,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"
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v4 09/14] dpll: dpllsim: Add support to set frequency on pins
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (7 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 08/14] selftests: dpll: add pin priority test Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 10/14] selftests: dpll: add pin frequency test Petr Oros
` (4 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 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. The frequency set over netlink is kept apart from the
configured one, like the priority, so the attribute keeps showing what
was configured and every live transition starts from it.
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- keep the frequency set over netlink apart from the configured one,
like the priority
- document that values set over netlink are not written back to the
attributes
- the Reviewed-by and Acked-by of v3 are not carried over, the
behaviour of the driver changed
v3: https://lore.kernel.org/all/20261109093523.931201-10-poros@redhat.com/
---
Documentation/driver-api/dpllsim.rst | 10 +++++-----
drivers/dpll/dpllsim/core.c | 4 ++--
drivers/dpll/dpllsim/dpll.c | 20 ++++++++++++++++++--
drivers/dpll/dpllsim/dpllsim.h | 8 ++++++--
4 files changed, 31 insertions(+), 11 deletions(-)
diff --git a/Documentation/driver-api/dpllsim.rst b/Documentation/driver-api/dpllsim.rst
index 27cfc4b2c71518..9c717b81a9473c 100644
--- a/Documentation/driver-api/dpllsim.rst
+++ b/Documentation/driver-api/dpllsim.rst
@@ -61,11 +61,11 @@ support, when a ``mux`` pin is an output or when an output pin has a
priority other than the default, and with ``ENOENT`` when a pin
directory is being created or removed at the same time.
-Every live transition starts from the configured values. A priority set
-over netlink while the instance is live is reported back over netlink,
-it is not written to the ``prio`` attribute and is gone once the
-instance is taken down. The driver stores the priority, it does not
-select a reference by it.
+Every live transition starts from the configured values. A frequency or
+a priority set over netlink while the instance is live is reported back
+over netlink, it is not written to the ``frequency`` or ``prio``
+attribute and is gone once the instance is taken down. The driver
+stores the priority, it does not select a reference by it.
Example
=======
diff --git a/drivers/dpll/dpllsim/core.c b/drivers/dpll/dpllsim/core.c
index fca16ec0976684..caf161221fe410 100644
--- a/drivers/dpll/dpllsim/core.c
+++ b/drivers/dpll/dpllsim/core.c
@@ -110,7 +110,7 @@ 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);
+ return sysfs_emit(page, "%llu\n", pin->cfg_frequency);
}
static ssize_t dpllsim_pin_frequency_store(struct config_item *item,
@@ -127,7 +127,7 @@ static ssize_t dpllsim_pin_frequency_store(struct config_item *item,
guard(mutex)(&pin->dev->lock);
if (dpllsim_dev_frozen(pin->dev))
return -EBUSY;
- pin->frequency = val;
+ pin->cfg_frequency = val;
return count;
}
CONFIGFS_ATTR(dpllsim_pin_, frequency);
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index 9fdb5dbeba4400..0cce8fef232cd3 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,
@@ -171,6 +184,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,
@@ -180,6 +194,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,
};
@@ -233,8 +248,8 @@ static bool dpllsim_pin_frequency_valid(struct dpllsim_pin *pin)
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)
+ if (pin->cfg_frequency >= prop.freq_supported[i].min &&
+ pin->cfg_frequency <= prop.freq_supported[i].max)
return true;
return false;
@@ -258,6 +273,7 @@ static void dpllsim_pin_reset(struct dpllsim_pin *pin)
{
pin->ops = dpllsim_pin_select_ops(pin);
pin->state = DPLL_PIN_STATE_DISCONNECTED;
+ pin->frequency = pin->cfg_frequency;
pin->prio = pin->cfg_prio;
}
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
index 54f4f1636f64e4..cd99227f1e4e1d 100644
--- a/drivers/dpll/dpllsim/dpllsim.h
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -27,7 +27,7 @@ struct dpllsim_pin {
/* Configuration, writable only until dpllsim_dev_frozen() */
enum dpll_pin_type type;
enum dpll_pin_direction direction;
- u64 frequency;
+ u64 cfg_frequency;
u32 cfg_prio;
/* Resolved at live=1 */
@@ -39,7 +39,11 @@ struct dpllsim_pin {
/* Simulated state, guarded by dev->lock */
enum dpll_pin_state state;
- /* Starts from cfg_prio at live=1, changed by the netlink setter */
+ /*
+ * Start from cfg_frequency and cfg_prio at live=1, changed by the
+ * netlink setters
+ */
+ u64 frequency;
u32 prio;
};
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v4 10/14] selftests: dpll: add pin frequency test
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (8 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 09/14] dpll: dpllsim: Add support to set frequency on pins Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 11/14] dpll: dpllsim: Add support to adjust phase Petr Oros
` (3 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, 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
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.
The core refuses a frequency outside the supported ranges, so a pin
whose current frequency is not in them could not be put back, zl3073x
reports a sub-Hz reference as 0 Hz and does not offer that. The test
bails out on such a pin before it touches it. The frequency belongs to
the pin and not to one of its parent devices, which is why the pin the
script picks must not be the reference of any of them.
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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- bail out before touching a pin whose current frequency is not among
the supported ones, it could not be restored
- tell a failed query from a pin without a frequency, check that the
value is a number and log a deferred restore that fails
- name the arguments of dpll_pin_set_frequency like the other setters
v3:
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v3: https://lore.kernel.org/all/20261109093523.931201-11-poros@redhat.com/
v2: https://lore.kernel.org/all/20261105151852.1844305-11-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-11-poros@redhat.com/
---
.../selftests/drivers/dpll/dpll_features.sh | 90 +++++++++++++++++++
.../selftests/drivers/dpll/dpll_lib.sh | 10 +++
2 files changed, 100 insertions(+)
diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
index f7340bd89602fe..ebb52c85262016 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_features.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
@@ -28,6 +28,7 @@ input_pins=()
ALL_TESTS="
test_pin_state_transitions
test_pin_priority_get_set
+ test_pin_frequency_set_get
"
##############################################################################
@@ -297,6 +298,95 @@ test_pin_state_transitions()
log_test "DPLL pin state transitions"
}
+test_pin_frequency_set_get()
+{
+ local before target after reason json
+
+ 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
+
+ # A query that failed is not a pin without a frequency.
+ if ! before=$(dpll_pin_get_frequency "$PIN_TUNE_ID"); then
+ check_err 1 "pin $PIN_TUNE_ID query failed"
+ log_test "DPLL pin frequency set/get"
+ return
+ fi
+ if [ -z "$before" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
+ "frequency not reported"
+ return
+ fi
+ if ! [[ "$before" =~ ^[0-9]+$ ]]; then
+ check_err 1 "frequency '$before' is not a number"
+ log_test "DPLL pin frequency set/get"
+ return
+ fi
+ if ! json=$(dpll_pin_json "$PIN_TUNE_ID"); then
+ check_err 1 "pin $PIN_TUNE_ID query failed"
+ log_test "DPLL pin frequency set/get"
+ return
+ fi
+ # The core refuses a frequency outside the supported ranges, the
+ # current one included, and a driver may report one it does not
+ # offer, zl3073x reads a sub-Hz reference back as 0 Hz. Such a pin
+ # could not be restored, so bail out before it is touched.
+ if ! jq -e --argjson cur "$before" \
+ 'any(.["frequency-supported"][]?;
+ .["frequency-min"] <= $cur and
+ .["frequency-max"] >= $cur)' <<< "$json" &> /dev/null; then
+ dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
+ "current frequency is not a supported one"
+ return
+ fi
+ defer dpll_restore 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=$(jq -r --argjson cur "$before" \
+ '[.["frequency-supported"][]? |
+ .["frequency-min"], .["frequency-max"] |
+ numbers | select(. != $cur)][0] // empty' <<< "$json")
+ 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/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index b8f4375a4f76a7..fa1895ab17f518 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -486,6 +486,16 @@ dpll_pin_set_prio()
prio "$prio" 2>&1 >/dev/null)
}
+# $2: frequency in Hz
+dpll_pin_set_frequency()
+{
+ local pin_id=$1
+ local frequency=$2
+
+ DPLL_LAST_ERR=$(dpll pin set id "$pin_id" frequency "$frequency" \
+ 2>&1 >/dev/null)
+}
+
##############################################################################
# dpllsim configfs interface
#
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v4 11/14] dpll: dpllsim: Add support to adjust phase
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (9 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 10/14] selftests: dpll: add pin frequency test Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 12/14] selftests: dpll: add pin phase adjust test Petr Oros
` (2 subsequent siblings)
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, 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
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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
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 0cce8fef232cd3..0260bffd157000 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),
};
@@ -182,6 +187,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,
@@ -190,6 +222,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 = {
@@ -197,6 +231,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 *
@@ -212,6 +248,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:
@@ -275,6 +314,7 @@ static void dpllsim_pin_reset(struct dpllsim_pin *pin)
pin->state = DPLL_PIN_STATE_DISCONNECTED;
pin->frequency = pin->cfg_frequency;
pin->prio = pin->cfg_prio;
+ 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 cd99227f1e4e1d..94383e71d7fcec 100644
--- a/drivers/dpll/dpllsim/dpllsim.h
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -45,6 +45,7 @@ struct dpllsim_pin {
*/
u64 frequency;
u32 prio;
+ s32 phase_adjust;
};
struct dpllsim_dev {
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v4 12/14] selftests: dpll: add pin phase adjust test
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (10 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 11/14] dpll: dpllsim: Add support to adjust phase Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 13/14] selftests: dpll: add concurrency stress tests Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 14/14] selftests: dpll: add reference leak tests Petr Oros
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, 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
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 outside the advertised range and one 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 either 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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- bail out when the current value lies outside the advertised range,
it could not be restored
- tell a failed query from a pin without a phase adjust or a range and
log a deferred restore that fails
v3:
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v3: https://lore.kernel.org/all/20261109093523.931201-13-poros@redhat.com/
v2: https://lore.kernel.org/all/20261105151852.1844305-13-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-13-poros@redhat.com/
---
.../selftests/drivers/dpll/dpll_features.sh | 102 ++++++++++++++++++
.../selftests/drivers/dpll/dpll_lib.sh | 15 +++
2 files changed, 117 insertions(+)
diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
index ebb52c85262016..7b7e077073db58 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_features.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
@@ -29,6 +29,7 @@ ALL_TESTS="
test_pin_state_transitions
test_pin_priority_get_set
test_pin_frequency_set_get
+ test_pin_phase_adjust_set_get
"
##############################################################################
@@ -298,6 +299,107 @@ 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
+
+ # A query that failed is not a pin without a phase adjust.
+ if ! before=$(dpll_pin_get_phase_adjust "$PIN_TUNE_ID") ||
+ ! json=$(dpll_pin_json "$PIN_TUNE_ID"); then
+ check_err 1 "pin $PIN_TUNE_ID query failed"
+ log_test "DPLL pin phase-adjust set/get"
+ return
+ fi
+ if [ -z "$before" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+ "phase adjust not reported"
+ return
+ fi
+ if ! [[ "$before" =~ ^-?[0-9]+$ ]]; then
+ check_err 1 "phase adjust '$before' is not a number"
+ log_test "DPLL pin phase-adjust set/get"
+ 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.
+ 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
+ # The core refuses a value outside the range or off the
+ # granularity, the current one included. A pin that sits at such a
+ # value could not be put back, so bail out before the restore is
+ # scheduled.
+ if [ "$before" -lt "$min" ] || [ "$before" -gt "$max" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+ "current phase adjust outside the adjust range"
+ return
+ fi
+ 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_restore 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 json
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index fa1895ab17f518..6b99dae5a4f379 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -442,6 +442,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
#
@@ -496,6 +501,16 @@ dpll_pin_set_frequency()
2>&1 >/dev/null)
}
+# $2: phase adjust in ps
+dpll_pin_set_phase_adjust()
+{
+ local pin_id=$1
+ local phase_adjust=$2
+
+ DPLL_LAST_ERR=$(dpll pin set id "$pin_id" phase-adjust \
+ "$phase_adjust" 2>&1 >/dev/null)
+}
+
##############################################################################
# dpllsim configfs interface
#
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v4 13/14] selftests: dpll: add concurrency stress tests
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (11 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 12/14] selftests: dpll: add pin phase adjust test Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 14/14] selftests: dpll: add reference leak tests Petr Oros
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, 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
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 with all its pins.
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. A worker that
a signal interrupts exits with the status of the signal, inside a trap
$? is the status of whatever ran last and the worker could pass for one
that finished. bash reaps a worker as soon as it exits, so a recorded
pid may have been handed out again by the time the handler runs, and it
signals a pid only while that is still a child of the script. 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.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- signal a recorded pid only while it is still a running child of the
script, bash reaps finished workers on its own and the pid may have
been reused
- rename the "maximum devices" test to "many devices" and check that
every device shows all its pins
- say what happens to a helper of a worker that is stuck in the kernel
- exit a signalled worker with the status of the signal, it could pass
for one that finished
v3:
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v2:
- quote the failed and ret expansions (netdev CI shellcheck)
v3: https://lore.kernel.org/all/20261109093523.931201-14-poros@redhat.com/
v2: https://lore.kernel.org/all/20261105151852.1844305-14-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-14-poros@redhat.com/
---
.../selftests/drivers/dpll/dpll_lib.sh | 30 ++-
.../selftests/drivers/dpll/dpll_stress.sh | 237 +++++++++++++++++-
2 files changed, 259 insertions(+), 8 deletions(-)
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 6b99dae5a4f379..1dfe15943f3431 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -55,8 +55,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=""
@@ -644,8 +646,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"
@@ -653,12 +659,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"
}
@@ -670,6 +676,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.
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index 1745643955da16..2abfacc43b48be 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
@@ -15,13 +15,24 @@ source "$SCRIPTDIR/dpll_lib.sh"
ALL_TESTS="
test_rapid_create_delete
+ test_concurrent_workers
test_pin_state_flooding
+ test_many_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,111 @@ 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 is a running child of this shell. bash
+# reaps a background child as soon as it exits and only keeps its status
+# for a later wait, so the pid of a worker that is gone can be handed
+# out again. A process that reused it has another parent, which is how
+# it is told apart, and it must never be signalled. A child that has
+# exited but was not 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 state ppid
+
+ # The state and the parent pid follow 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
+ read -r state ppid _ <<< "$stat"
+ [ "$ppid" = "$$" ] && [ "$state" != "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)
+ # Only a process that is still a running child of this shell is
+ # signalled, see pid_running, a recorded pid may have been reused
+ # since its worker finished.
+ for pid in "${pids[@]}"; do
+ pid_running "$pid" && 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. A
+ # worker stuck in the kernel does not die of the forced kill
+ # before whatever it waits for returns, and neither does a helper
+ # it runs, the mkdir of a device for one, which outlives the
+ # worker then. So only the workers that are gone are waited for
+ # and the teardown goes on without the rest. The removal of what
+ # such a worker or helper left behind can then fail or race with
+ # it, which is logged like any other failed removal, and what a
+ # stuck helper creates afterwards stays behind.
+ pids_gone "${pids[@]}"
+ for pid in "${pids[@]}"; do
+ pid_running "$pid" && 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 +242,88 @@ 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. TERM and INT exit through it with the
+# status a shell reports for the signal, inside a trap $? would be the
+# status of whatever ran last and an interrupted worker could pass for
+# one that finished.
+worker_exit()
+{
+ local status=$?
+
+ trap - EXIT
+ trap '' 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
+ trap 'exit 143' TERM
+ trap 'exit 130' 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_ids pin_id i last state fails=0
@@ -209,6 +395,55 @@ test_pin_state_flooding()
log_test "DPLL pin state flooding"
}
+test_many_devices()
+{
+ local i clock_id created=0 visible dev_id pin_ids
+ 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 with all its
+ # pins.
+ visible=0
+ for clock_id in "${clock_ids[@]}"; do
+ dev_id=$(dpll_device_wait_by_clock_id "$clock_id" dpllsim) ||
+ continue
+ visible=$((visible + 1))
+ dpll_pin_ids_read pin_ids dpll_pin_get_ids "$dev_id"
+ check_err $? "pin dump failed"
+ if [ "${#pin_ids[@]}" -ne "$STRESS_PINS_PER_DEVICE" ]; then
+ check_err 1 "device $dev_id has ${#pin_ids[@]} pins"
+ 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 many devices ($STRESS_DEVICES devices)"
+}
+
test_deploy_cycle()
{
local clock_id name i
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH net-next v4 14/14] selftests: dpll: add reference leak tests
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (12 preceding siblings ...)
2026-11-12 13:03 ` [PATCH net-next v4 13/14] selftests: dpll: add concurrency stress tests Petr Oros
@ 2026-11-12 13:03 ` Petr Oros
13 siblings, 0 replies; 15+ messages in thread
From: Petr Oros @ 2026-11-12 13:03 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, 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
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. The teardown of a device releases its
references before the write to live returns, so the entries are
expected to be gone at the first look; the check still polls for a
while before it reports a leak, so that it does not fail on a single
read. Attributing by name means that a leak of an earlier run goes
unnoticed, that an object reusing the address of a freed one from the
snapshot can hide a leak and that an object another driver registers
while a script runs is reported as a leak, which is accepted, the
entries carry nothing better to go by. ref_tracker removes the file of
a freed object from a work item and a read of it fails until then, so
the snapshot waits for such files to go away and records only entries
it can read, or the file of the probe device could get into it and hide
the next object allocated at that address. 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 entries exist without
CONFIG_DPLL_REFCNT_TRACKER too and just stay empty, so a probe that saw
no reference is a skip unless /proc/config.gz says the tracker is built
in, which makes it a failure. The probe runs as the first test of the
stress script, which gains a final leak check that is skipped when an
earlier test left a device behind, 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, debugfs and CONFIG_IKCONFIG_PROC for the look at
/proc/config.gz.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v4:
- wait for the files of freed objects to go away before a snapshot and
record only entries that can be read
- a probe that saw no reference is a failure when /proc/config.gz says
the tracker is built in, a skip otherwise
- skip the final leak check of the stress script when an earlier test
left a device behind
- record the debugfs mount before mounting
- say that objects another driver registers during a run are reported
- enable CONFIG_IKCONFIG_PROC in the selftest config, the probe reads
/proc/config.gz
- say that the probe device could not be created or deployed, not only
created
- add the Reviewed-by given to v3 (Ivan Vecera)
v3:
- say what the leak check waits for instead of a concurrent dump
holding references, which nothing does (Ivan Vecera)
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v2:
- keep the config file sorted and quote the ret expansions (netdev CI
shellcheck)
v3: https://lore.kernel.org/all/20261109093523.931201-15-poros@redhat.com/
v2: https://lore.kernel.org/all/20261105151852.1844305-15-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-15-poros@redhat.com/
---
tools/testing/selftests/drivers/dpll/Makefile | 1 +
tools/testing/selftests/drivers/dpll/config | 5 +
.../selftests/drivers/dpll/dpll_lib.sh | 308 ++++++++++++++++--
.../selftests/drivers/dpll/dpll_reftrack.sh | 190 +++++++++++
.../selftests/drivers/dpll/dpll_stress.sh | 105 +++++-
5 files changed, 584 insertions(+), 25 deletions(-)
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_reftrack.sh
diff --git a/tools/testing/selftests/drivers/dpll/Makefile b/tools/testing/selftests/drivers/dpll/Makefile
index 1349d078fa2660..d292ea049edfa3 100644
--- a/tools/testing/selftests/drivers/dpll/Makefile
+++ b/tools/testing/selftests/drivers/dpll/Makefile
@@ -10,6 +10,7 @@ TEST_FILES := dpll_lib.sh
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/dpll/config b/tools/testing/selftests/drivers/dpll/config
index e2c90b6970bd43..69ff54875712c6 100644
--- a/tools/testing/selftests/drivers/dpll/config
+++ b/tools/testing/selftests/drivers/dpll/config
@@ -1,3 +1,8 @@
CONFIG_CONFIGFS_FS=y
+CONFIG_DEBUG_FS=y
+CONFIG_DEBUG_KERNEL=y
CONFIG_DPLL=y
+CONFIG_DPLL_REFCNT_TRACKER=y
CONFIG_DPLLSIM=m
+CONFIG_IKCONFIG=y
+CONFIG_IKCONFIG_PROC=y
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 1dfe15943f3431..f7bfe046edf936 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -16,6 +16,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
@@ -53,12 +55,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=""
@@ -523,10 +525,11 @@ dpll_pin_set_phase_adjust()
# device is not live.
#
# dpllsim_available loads the dpllsim module and mounts configfs when
-# nothing else did. Scripts call dpll_lib_cleanup last from their exit
-# trap to undo the mount, the module stays loaded. The only other trace
-# the library leaves outside the dpllsim directory is DPLL_RUN_DIR with
-# the lock file.
+# nothing else did, dpll_debugfs_available below does the same for
+# debugfs. Scripts call dpll_lib_cleanup last from their exit trap to
+# undo the mounts, the module stays loaded. The only other trace the
+# library leaves outside the dpllsim directory is DPLL_RUN_DIR with the
+# lock file.
DPLLSIM_MOUNTED_CONFIGFS=0
# Name of the device dpllsim_create_device created last. Set here, so
@@ -549,26 +552,40 @@ 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()
+{
+ # The mount recorded up front may have failed.
+ mountpoint -q "$1" || return 0
+ 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
- # The mount recorded up front may have failed.
- mountpoint -q "$CONFIGFS_DIR" || return 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()
@@ -719,3 +736,254 @@ 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.
+# An object another driver registers while a script runs cannot be told
+# from one of the script and is reported as a leak, the scripts need a
+# system where no other DPLL driver is probed or reloaded meanwhile.
+#
+# ref_tracker removes the file of a freed object from a work item. Until
+# that ran the file is still there and a read of it fails. Such an entry
+# must not get into a snapshot, an object allocated at the same address
+# later has the same name and would be ignored.
+
+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
+ # Recorded before the mount, so that a cleanup run by a
+ # signal right after it knows about the mount.
+ DPLL_MOUNTED_DEBUGFS=1
+ mount -t debugfs none "$DEBUGFS_DIR" &> /dev/null ||
+ DPLL_MOUNTED_DEBUGFS=0
+ fi
+ mountpoint -q "$DEBUGFS_DIR"
+}
+
+__dpll_ref_tracker_hash()
+{
+ printf '%s' "$1" | md5sum | cut -d' ' -f1
+}
+
+# Return 0 when every dpll_device@* / dpll_pin@* entry can be read,
+# which means that no file of a freed object is waiting for its removal.
+__dpll_ref_tracker_reaped()
+{
+ local entry
+
+ for entry in "$DPLL_REF_TRACKER_DIR"/dpll_device@* \
+ "$DPLL_REF_TRACKER_DIR"/dpll_pin@*; do
+ [ -f "$entry" ] || continue
+ cat "$entry" &> /dev/null || return 1
+ done
+}
+
+# Write "<name> <content hash>" for every dpll_device@* / dpll_pin@*
+# entry of a live object to the given file, one entry per line. The
+# files of freed objects are given a while to go away first, one that is
+# still there afterwards cannot be read and is left out. The file ends
+# up empty when the tracker dir is missing.
+dpll_ref_tracker_snapshot()
+{
+ local file=$1
+ local entry content
+
+ busywait "$BUSYWAIT_TIMEOUT" __dpll_ref_tracker_reaped > /dev/null
+ 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) || continue
+ 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. The teardown of a
+# device releases its references before the write to live returns, so
+# the entries are expected to be gone at the first look; the check still
+# polls for a while rather than failing on that look, so that a leak is
+# reported only when the references stay. A timeout means they 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"
+}
+
+# Return 0 when the kernel configuration says that the DPLL reference
+# tracker is built in. The entries exist without it as well, they just
+# stay empty, so a probe that saw no reference cannot tell a kernel
+# without the tracker from a tracker that records nothing. Return 1
+# when the tracker is not configured or there is no /proc/config.gz to
+# ask.
+dpll_ref_tracker_configured()
+{
+ zcat /proc/config.gz 2>/dev/null |
+ grep -q '^CONFIG_DPLL_REFCNT_TRACKER=y'
+}
+
+# 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, which dpll_ref_tracker_configured helps to interpret, 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"
+}
diff --git a/tools/testing/selftests/drivers/dpll/dpll_reftrack.sh b/tools/testing/selftests/drivers/dpll/dpll_reftrack.sh
new file mode 100755
index 00000000000000..f2871b5ad84723
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/dpll_reftrack.sh
@@ -0,0 +1,190 @@
+#!/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/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)
+ # The probe device held no tracked reference. That is a
+ # kernel without the tracker, unless its configuration
+ # says otherwise.
+ if ! dpll_ref_tracker_configured; then
+ log_test_skip "DPLL ref_tracker probe" \
+ "no DPLL reference tracked"
+ return
+ fi
+ check_err 1 "tracker enabled but no reference recorded"
+ ;;
+ 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 or deploy the 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/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index 2abfacc43b48be..226e2767ee8970 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
@@ -14,11 +14,13 @@ source "$SCRIPTDIR/../../net/lib.sh"
source "$SCRIPTDIR/dpll_lib.sh"
ALL_TESTS="
+ test_ref_tracker_probe
test_rapid_create_delete
test_concurrent_workers
test_pin_state_flooding
test_many_devices
test_deploy_cycle
+ test_ref_leaks
"
STRESS_ITERATIONS=${STRESS_ITERATIONS:-20}
@@ -161,6 +163,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
}
@@ -216,6 +223,66 @@ 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)
+ # The probe device held no tracked reference. That is a
+ # kernel without the tracker, unless its configuration
+ # says otherwise.
+ if ! dpll_ref_tracker_configured; then
+ log_test_skip "DPLL ref_tracker probe" \
+ "no DPLL reference tracked"
+ return
+ fi
+ check_err 1 "tracker enabled but no reference recorded"
+ ;;
+ 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 or deploy the 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
@@ -496,16 +563,44 @@ 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
+
+ # A device an earlier test could not remove still holds its
+ # references, the failure to report is that one.
+ if [ "${#CREATED_DEVICES[@]}" -gt 0 ]; then
+ log_test_skip "DPLL stress ref_tracker check" \
+ "device from an earlier test still present"
+ 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
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-11-12 13:05 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-12 13:03 [PATCH net-next v4 00/14] dpll: add a simulated DPLL device driver Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 01/14] dpll: dpllsim: Add " Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 03/14] dpll: dpllsim: Add configurable pins Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 04/14] selftests: dpll: add pin enumeration and property tests Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 05/14] dpll: dpllsim: Add support to set input pin state Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 06/14] selftests: dpll: add pin state tests Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 07/14] dpll: dpllsim: Add support to get/set priority on input pins Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 08/14] selftests: dpll: add pin priority test Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 09/14] dpll: dpllsim: Add support to set frequency on pins Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 10/14] selftests: dpll: add pin frequency test Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 11/14] dpll: dpllsim: Add support to adjust phase Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 12/14] selftests: dpll: add pin phase adjust test Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 13/14] selftests: dpll: add concurrency stress tests Petr Oros
2026-11-12 13:03 ` [PATCH net-next v4 14/14] selftests: dpll: add reference leak tests Petr Oros
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox