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