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..874007411bc5e7
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
@@ -0,0 +1,256 @@
+#!/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 simulator-only tests then skip unless that device
+# is a dpllsim one.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../net/lib.sh"
+source "$SCRIPTDIR/dpll_lib.sh"
+
+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
+}
+
+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
+ if ! dpll_pin_has_capability "$PIN_TUNE_ID" state-can-change; then
+ dpll_log_test_skip_or_fail "DPLL pin state transitions" \
+ "state cannot change"
+ return
+ fi
+
+ # 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
+ # Restore the original state even when the test aborts half way.
+ defer dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"
+
+ # 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, after the restore above 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_pin_set_state "$DPLL_DEV_ID" "$ref_pin" \
+ connected
+ fi
+ fi
+
+ # 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 refused restore is treated the same
+ # way when nothing failed before it, afterwards it is one more
+ # failure, a skip would hide the earlier one.
+ for state in $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
+ after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ "$after" != "$state" ]; then
+ check_err 1 "pin not $state, state '$after'"
+ fi
+ done
+
+ # 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
+ after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ "$after" != "$before" ]; then
+ check_err 1 "pin state not restored, got '$after'"
+ fi
+
+ 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
+if [ -z "${DPLL_DEV_ID:-}" ]; then
+ dpllsim_check_module
+ if ! dpllsim_setup_standard_device; then
+ echo "SKIP: cannot create dpllsim device"
+ exit "$ksft_skip"
+ fi
+ DPLL_DEV_ID=$DPLLSIM_DEV_ID
+ DPLL_CREATED_DEVICE=$DPLLSIM_CREATED_NAME
+ DPLL_MODULE_NAME="dpllsim"
+fi
+
+log_info "testing DPLL device $DPLL_DEV_ID (module $DPLL_MODULE_NAME)"
+
+# PIN_TUNE_ID is the input pin the tests reconfigure. It must not be the
+# reference the device is locked to, so it is the first input pin that
+# is not connected on the device. When the first dumped input pin and
+# another one both qualify the other one is taken, so that the first
+# pin, the one a simulated device locks to once it is selectable, is
+# left alone. Tests that need the pin skip when there is none rather
+# than reconfiguring the reference.
+pick_tune_pin()
+{
+ local pin_id state
+
+ PIN_TUNE_ID=""
+ for pin_id in "${input_pins[@]}"; do
+ state=$(dpll_pin_get_state "$DPLL_DEV_ID" "$pin_id")
+ case "$state" in
+ disconnected|selectable)
+ ;;
+ *)
+ continue
+ ;;
+ esac
+ PIN_TUNE_ID=$pin_id
+ [ "$pin_id" != "${input_pins[0]}" ] && return
+ done
+}
+
+mapfile -t input_pins < <(dpll_pin_find_by_direction "$DPLL_DEV_ID" input)
+pick_tune_pin
+
+tests_run
+
+exit $EXIT_STATUSdiff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 0db4a7e2e12fcf..63c70a4aaab6e2 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -5,7 +5,8 @@
# tool and around the dpllsim configfs interface for creating virtual DPLL
# devices. Sourcing this file has no side effects; call
# dpll_lib_check_prereqs and dpll_lib_init explicitly. net/lib.sh must be
-# sourced first, it provides ksft_skip, busywait and log_info.
+# sourced first, it provides ksft_skip, busywait and log_info to the
+# library and defer to the scripts.
#
# Copyright (c) 2026 Red Hat, Inc.
#
@@ -86,6 +87,44 @@ dpll_lib_lock()
fi
}
+# Print the usage block of the given command of the given object from
+# the help of the dpll tool, "dpll pin set" for instance. The help lists
+# one usage per command, each starting with "Usage: dpll <obj> <cmd>" or
+# with "dpll <obj> <cmd>" indented under the first, possibly wrapped
+# over further lines. The block starts at the line that names the
+# command, with a space after the name so that a command whose name
+# merely begins the same way does not match, and ends before the next
+# line that names another command of the object, or at the end of the
+# help when none follows. Return 1 when the help has no usage for the
+# command, in which case nothing is printed.
+__dpll_usage_block()
+{
+ local obj=$1
+ local cmd=$2
+
+ dpll "$obj" help 2>&1 | \
+ awk -v start="[[:space:]]dpll $obj $cmd([[:space:]]|$)" \
+ -v other="[[:space:]]dpll $obj [a-z-]+([[:space:]]|$)" \
+ '$0 ~ start { s = 1; print; next }
+ s && $0 ~ other { exit }
+ s { print }
+ END { exit !s }'
+}
+
+# Return 0 when the dpll tool knows the given "pin set" keyword, such as
+# state or frequency. A test that sets something checks its keyword
+# first and skips on a tool that is too old for it. Only the "pin set"
+# usage is searched, a keyword that only "pin show" knows must not
+# count, and the keyword must stand on its own between blanks or
+# brackets, a hyphenated compound that contains it does not count.
+dpll_pin_set_supports()
+{
+ local usage
+
+ usage=$(__dpll_usage_block pin set) || return 1
+ grep -qE "(^|[[:space:][])$1([[:space:]]|$)" <<< "$usage"
+}
+
# Detect the DPLL device to test. DPLL_DEV_ID may be set by the user to
# force a specific device. Otherwise DPLL_DRIVER names the driver whose
# first device is taken, by the module name the device reports, dpllsim@@ -183,6 +222,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@@ -337,6 +381,41 @@ dpll_pin_get_state()
__dpll_pin_dev_field "$1" "$2" '.state'
}
+# Return 0 when the pin advertises the given capability, named as
+# iproute2 prints it (state-can-change, priority-can-change, ...).
+dpll_pin_has_capability()
+{
+ local pin_id=$1
+ local name=$2
+ local out
+
+ out=$(dpll pin show id "$pin_id" -j 2>/dev/null) || return 1
+ jq -e --arg name "$name" \
+ '(.pin[0]? // .) | .capabilities | index($name) != null' \
+ <<< "$out" &> /dev/null
+}
+
+##############################################################################
+# Pin setters
+#
+# Every setter returns the status of the tool and keeps what the tool
+# printed on stderr in DPLL_LAST_ERR, the "Error: ..." line that carries
+# the extack message of the kernel, so that a caller can tell a refusal
+# it may skip on from a failure.
+
+DPLL_LAST_ERR=""
+
+# $3: connected / disconnected / selectable
+dpll_pin_set_state()
+{
+ local dev_id=$1
+ local pin_id=$2
+ local state=$3
+
+ DPLL_LAST_ERR=$(dpll pin set id "$pin_id" parent-device "$dev_id" \
+ state "$state" 2>&1 >/dev/null)
+}
+
##############################################################################
# dpllsim configfs interface
#diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index b21325d8368fb2..526cb113807f72 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,62 @@ test_rapid_create_delete()
log_test "DPLL rapid create/delete ($STRESS_ITERATIONS iterations)"
}
+test_pin_state_flooding()
+{
+ local clock_id name dev_id pin_id i last state fails=0
+ # The states of automatic mode, the mode the simulator runs in
+ local states=(disconnected selectable)
+
+ RET=0
+
+ if ! dpll_pin_set_supports state; then
+ log_test_skip "DPLL pin state flooding" \
+ "dpll tool lacks pin set state"
+ return
+ fi
+
+ dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
+ if ! create_stress_device "$clock_id" 10; then
+ check_err 1 "cannot create device"
+ log_test "DPLL pin state flooding"
+ return
+ fi
+ name=$DPLLSIM_CREATED_NAME
+ track_device "$name"
+
+ if ! dev_id=$(dpll_device_wait_by_clock_id "$clock_id" dpllsim); then
+ check_err 1 "device not visible on netlink"
+ else
+ pin_id=$(dpll_pin_find_by_direction "$dev_id" input | head -1)
+ if [ -z "$pin_id" ]; then
+ check_err 1 "no input pin on stress device"
+ else
+ for i in $(seq 1 50); do
+ last=${states[i % 2]}
+ dpll_pin_set_state "$dev_id" "$pin_id" \
+ "$last" ||
+ fails=$((fails + 1))
+ done
+ if [ "$fails" -gt 0 ]; then
+ check_err 1 "$fails of 50 state changes failed"
+ fi
+ state=$(dpll_pin_get_state "$dev_id" "$pin_id")
+ if [ "$state" != "$last" ]; then
+ check_err 1 \
+ "state expected '$last', got '$state'"
+ fi
+ fi
+ fi
+
+ if dpllsim_delete_device "$name"; then
+ untrack_device "$name"
+ else
+ check_err 1 "cannot delete device $name"
+ fi
+
+ log_test "DPLL pin state flooding"
+}
+
test_deploy_cycle()
{
local clock_id name i--
2.56.0