[PATCH net-next v3 06/14] selftests: dpll: add pin state tests

From
Petr Oros <hidden>
Date
2026-11-09 09:36:27
Also in
linux-doc, linux-kselftest
Reply to
Petr Oros, 2026-11-09
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, the first input
pin that is not connected on the device, so that no test touches the
reference the device is locked to.

The first test moves that pin 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. The restores are deferred right after the original states are
read, and the cleanup handler of every script that defers runs the
pending restores before it removes the devices the script created, so
that an interrupted run puts the device back as well.

The stress script gains a test that floods one input pin of a fresh
device with changes between the states of automatic mode. 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.

Signed-off-by: Petr Oros <redacted>
---
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)

v2: https://lore.kernel.org/all/20261105151852.1844305-7-poros@redhat.com/ (local)
v1: https://lore.kernel.org/all/20261103093910.596225-7-poros@redhat.com/ (local)
---
 tools/testing/selftests/drivers/dpll/Makefile |   1 +
 .../selftests/drivers/dpll/dpll_features.sh   | 256 ++++++++++++++++++
 .../selftests/drivers/dpll/dpll_lib.sh        |  81 +++++-
 .../selftests/drivers/dpll/dpll_stress.sh     |  58 ++++
 4 files changed, 395 insertions(+), 1 deletion(-)
 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..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_STATUS
diff --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
Thread (19 messages) 19 messages, 2 authors, last reply 22h ago
Keyboard shortcuts
hback out one level
jnext message in thread / next series-diff position
kprevious message in thread / previous series-diff position
ldrill in
Escclose help / fold thread tree
?toggle this help