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