From d963364882d32146ca7697389a03546ca3164c6b Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Fri, 18 Sep 2026 13:31:42 +0530 Subject: [PATCH 1/4] utils: add shared QRTR validation helpers Provide bounded native and Python topology capture, normalized service evidence, and reusable QRTR and PD Mapper runtime helpers. Retain bounded log excerpts and distinguish clean kernel logs from unavailable capture, including a retry for successful-but-empty dmesg output. Signed-off-by: Srikanth Muppandam --- Runner/utils/functestlib.sh | 210 ++++++++++-- Runner/utils/lib_qrtr.sh | 667 ++++++++++++++++++++++++++++++++++++ Runner/utils/qrtr_lookup.py | 78 +++++ 3 files changed, 933 insertions(+), 22 deletions(-) create mode 100755 Runner/utils/lib_qrtr.sh create mode 100755 Runner/utils/qrtr_lookup.py diff --git a/Runner/utils/functestlib.sh b/Runner/utils/functestlib.sh index 414f8bbf..7ceaeae1 100755 --- a/Runner/utils/functestlib.sh +++ b/Runner/utils/functestlib.sh @@ -2890,10 +2890,13 @@ run_with_timeout_log() { run_with_timeout "$rwtl_timeout" "$@" > "$rwtl_log_file" 2>&1 } -# Purpose: Replay every line from a file through the common information logger. +# Purpose: Replay a bounded number of lines from a file through the common +# information logger. # Arguments: # $1 - Label prepended to each logged line. # $2 - Log-file path to replay. +# $3 - Optional maximum lines. Empty or zero preserves the historical full +# replay behavior. # Output: # Sends each readable input line through log_info(). # Returns: @@ -2901,12 +2904,38 @@ run_with_timeout_log() { log_file_with_label() { lfwl_label="$1" lfwl_file="$2" + lfwl_max_lines="${3:-0}" + lfwl_total=0 + lfwl_emitted=0 [ -r "$lfwl_file" ] || return 0 + case "$lfwl_max_lines" in + ''|*[!0-9]*) + lfwl_max_lines=0 + ;; + esac + + lfwl_total=$(wc -l <"$lfwl_file" 2>/dev/null | tr -d '[:space:]') + case "$lfwl_total" in + ''|*[!0-9]*) + lfwl_total=0 + ;; + esac + while IFS= read -r lfwl_line || [ -n "$lfwl_line" ]; do + if [ "$lfwl_max_lines" -gt 0 ] && + [ "$lfwl_emitted" -ge "$lfwl_max_lines" ]; then + break + fi log_info "[$lfwl_label] $lfwl_line" + lfwl_emitted=$((lfwl_emitted + 1)) done < "$lfwl_file" + if [ "$lfwl_max_lines" -gt 0 ] && + [ "$lfwl_total" -gt "$lfwl_emitted" ]; then + log_info "[$lfwl_label] omitted=$((lfwl_total - lfwl_emitted)) total=$lfwl_total artifact=$lfwl_file" + fi + return 0 } @@ -7069,14 +7098,16 @@ detect_ufs_partition_block() { return 1 } -############################################################################### -# scan_dmesg_errors -# -# Only scans *new* dmesg lines for true error patterns (since last test run). -# Keeps a timestamped error log history for each run. -# Handles dmesg with/without timestamps. Cleans up markers/logs if test dir is gone. -# Usage: scan_dmesg_errors "$SCRIPT_DIR" [optional_extra_keywords...] -############################################################################### +# scan_dmesg_errors OUTPUT_DIR MODULE_REGEX [EXCLUDE_REGEX] +# Capture the kernel log once and report non-benign errors for selected modules. +# Inputs: retained output directory, extended module regex, and optional exclusion +# regex. Set KERNEL_LOG_JOURNAL_FALLBACK=1 to permit journalctl fallback. +# Outputs: retained snapshot, filtered errors, access diagnostics, timestamped +# history, and exported DMESG_ACCESS_STATUS, DMESG_ACCESS_RC, +# DMESG_ACCESS_PROVIDER, and DMESG_ACCESS_LOG values. +# Returns: 0 when matching errors are found and 1 for a clean or unavailable +# capture. Callers must inspect DMESG_ACCESS_STATUS to distinguish those cases. +# Side effects: replaces kernel-log artifacts below OUTPUT_DIR and emits logs. scan_dmesg_errors() { prefix="$1" module_regex="$2" # e.g. 'qcom_camss|camss|isp' @@ -7087,14 +7118,117 @@ scan_dmesg_errors() { DMESG_SNAPSHOT="$prefix/dmesg_snapshot.log" DMESG_ERRORS="$prefix/dmesg_errors.log" + DMESG_ACCESS_LOG="$prefix/dmesg_access.log" + DMESG_JOURNAL_RAW="$prefix/journalctl_kernel.log" DATE_STAMP=$(date +%Y%m%d-%H%M%S) DMESG_HISTORY="$prefix/dmesg_errors_$DATE_STAMP.log" # Error patterns (edit as needed for your test coverage) err_patterns='Unknown symbol|probe failed|fail(ed)?|error|timed out|not found|invalid|corrupt|abort|panic|oops|unhandled|can.t (start|init|open|allocate|find|register)' - rm -f "$DMESG_SNAPSHOT" "$DMESG_ERRORS" - dmesg > "$DMESG_SNAPSHOT" 2>/dev/null + DMESG_ACCESS_STATUS="unknown" + DMESG_ACCESS_RC="unknown" + DMESG_ACCESS_PROVIDER="none" + export DMESG_ACCESS_STATUS DMESG_ACCESS_RC DMESG_ACCESS_PROVIDER + export DMESG_ACCESS_LOG + + rm -f \ + "$DMESG_SNAPSHOT" \ + "$DMESG_ERRORS" \ + "$DMESG_ACCESS_LOG" \ + "$DMESG_JOURNAL_RAW" + : >"$DMESG_SNAPSHOT" + : >"$DMESG_ERRORS" + : >"$DMESG_ACCESS_LOG" + + sde_dmesg_command=$(command -v dmesg 2>/dev/null || true) + if [ -n "$sde_dmesg_command" ]; then + "$sde_dmesg_command" >"$DMESG_SNAPSHOT" 2>"$DMESG_ACCESS_LOG" + DMESG_ACCESS_RC=$? + DMESG_ACCESS_PROVIDER="dmesg" + else + DMESG_ACCESS_RC=127 + DMESG_ACCESS_PROVIDER="dmesg" + printf 'provider=dmesg status=command-not-found rc=127\n' \ + >>"$DMESG_ACCESS_LOG" + fi + + sde_snapshot_bytes=$(wc -c <"$DMESG_SNAPSHOT" 2>/dev/null | tr -d '[:space:]') + { + printf 'capture=direct rc=%s command=%s snapshot_bytes=%s uid=%s\n' \ + "$DMESG_ACCESS_RC" \ + "${sde_dmesg_command:-not-found}" \ + "${sde_snapshot_bytes:-0}" \ + "$(id -u 2>/dev/null || printf 'unknown')" + if [ -r /proc/sys/kernel/dmesg_restrict ]; then + printf 'dmesg_restrict=%s\n' \ + "$(cat /proc/sys/kernel/dmesg_restrict 2>/dev/null)" + fi + if [ -r /proc/self/status ]; then + grep '^CapEff:' /proc/self/status 2>/dev/null || true + fi + } >>"$DMESG_ACCESS_LOG" + + # A few target images have returned success with no redirected output even + # though an interactive dmesg invocation is readable. Retry through command + # substitution so the shell, rather than dmesg, writes the retained file. + if [ "$DMESG_ACCESS_RC" -eq 0 ] && [ ! -s "$DMESG_SNAPSHOT" ]; then + sde_retry_output=$("$sde_dmesg_command" 2>>"$DMESG_ACCESS_LOG") + sde_retry_rc=$? + if [ "$sde_retry_rc" -eq 0 ] && [ -n "$sde_retry_output" ]; then + printf '%s\n' "$sde_retry_output" >"$DMESG_SNAPSHOT" + fi + sde_retry_bytes=$(wc -c <"$DMESG_SNAPSHOT" 2>/dev/null | tr -d '[:space:]') + printf 'capture=shell-buffer-retry rc=%s snapshot_bytes=%s\n' \ + "$sde_retry_rc" \ + "${sde_retry_bytes:-0}" >>"$DMESG_ACCESS_LOG" + DMESG_ACCESS_RC=$sde_retry_rc + fi + + if [ "$DMESG_ACCESS_RC" -ne 0 ] || [ ! -s "$DMESG_SNAPSHOT" ]; then + : >"$DMESG_SNAPSHOT" + if [ "${KERNEL_LOG_JOURNAL_FALLBACK:-0}" = "1" ] && + command -v journalctl >/dev/null 2>&1; then + sde_journal_command=$(command -v journalctl 2>/dev/null || true) + journalctl -k -b --no-pager -o cat \ + >"$DMESG_JOURNAL_RAW" 2>>"$DMESG_ACCESS_LOG" + sde_journal_rc=$? + sde_journal_bytes=$(wc -c <"$DMESG_JOURNAL_RAW" 2>/dev/null | tr -d '[:space:]') + printf 'provider=journalctl rc=%s snapshot_bytes=%s command=%s\n' \ + "$sde_journal_rc" \ + "${sde_journal_bytes:-0}" \ + "${sde_journal_command:-not-found}" >>"$DMESG_ACCESS_LOG" + DMESG_ACCESS_RC=$sde_journal_rc + DMESG_ACCESS_PROVIDER="journalctl" + if [ "$sde_journal_rc" -eq 0 ] && [ -s "$DMESG_JOURNAL_RAW" ]; then + sed 's/^/[journal] /' \ + "$DMESG_JOURNAL_RAW" >"$DMESG_SNAPSHOT" + DMESG_ACCESS_RC=0 + fi + elif [ "${KERNEL_LOG_JOURNAL_FALLBACK:-0}" = "1" ]; then + DMESG_ACCESS_RC=127 + DMESG_ACCESS_PROVIDER="journalctl" + printf 'provider=journalctl status=command-not-found rc=127\n' \ + >>"$DMESG_ACCESS_LOG" + else + printf 'provider=journalctl status=disabled\n' \ + >>"$DMESG_ACCESS_LOG" + fi + fi + + if [ ! -s "$DMESG_SNAPSHOT" ]; then + DMESG_ACCESS_STATUS="unavailable" + export DMESG_ACCESS_STATUS DMESG_ACCESS_RC DMESG_ACCESS_PROVIDER + cp "$DMESG_ERRORS" "$DMESG_HISTORY" + log_warn "[DMESG-ACCESS] status=$DMESG_ACCESS_STATUS provider=$DMESG_ACCESS_PROVIDER rc=$DMESG_ACCESS_RC snapshot_bytes=0 artifact=$DMESG_ACCESS_LOG" + log_file_with_label "DMESG-ACCESS" "$DMESG_ACCESS_LOG" 12 + return 1 + fi + DMESG_ACCESS_STATUS="available" + export DMESG_ACCESS_STATUS DMESG_ACCESS_RC DMESG_ACCESS_PROVIDER + if [ "${KERNEL_LOG_JOURNAL_FALLBACK:-0}" = "1" ]; then + log_info "[DMESG-ACCESS] status=$DMESG_ACCESS_STATUS provider=$DMESG_ACCESS_PROVIDER snapshot_bytes=$(wc -c <"$DMESG_SNAPSHOT" | tr -d '[:space:]') artifact=$DMESG_ACCESS_LOG" + fi # 1. Match lines with correct module and error pattern # 2. Exclude lines with harmless patterns (using dummy regulator etc) @@ -7104,13 +7238,21 @@ scan_dmesg_errors() { cp "$DMESG_ERRORS" "$DMESG_HISTORY" if [ -s "$DMESG_ERRORS" ]; then - log_info "dmesg scan: found non-benign module errors in $DMESG_ERRORS (history: $DMESG_HISTORY)" + if [ "$DMESG_ACCESS_PROVIDER" = "dmesg" ]; then + log_info "dmesg scan: found non-benign module errors in $DMESG_ERRORS (history: $DMESG_HISTORY)" + else + log_info "Kernel-log scan found non-benign module errors in $DMESG_ERRORS (history: $DMESG_HISTORY)" + fi while IFS= read -r line; do log_info "[dmesg] $line" done < "$DMESG_ERRORS" return 0 fi - log_info "No relevant, non-benign errors for modules [$module_regex] in recent dmesg." + if [ "$DMESG_ACCESS_PROVIDER" = "dmesg" ]; then + log_info "No relevant, non-benign errors for modules [$module_regex] in recent dmesg." + else + log_info "No relevant, non-benign errors for modules [$module_regex] in the captured kernel log." + fi return 1 } @@ -7347,13 +7489,18 @@ qrtr_runtime_present() { } # qrtr_capture_topology [timeout-seconds] -# Runs one bounded, read-only qrtr-lookup inventory and validates its tabular -# header. Returns 0 for a valid snapshot, 1 for a broken query, 2 when QRTR or -# qrtr-lookup is unavailable, and 3 for invalid arguments. +# Runs one bounded, read-only QRTR control lookup and validates its tabular +# header. The image-provided qrtr-lookup is preferred, with the bundled public +# AF_QIPCRTR client as a fallback. Returns 0 for a valid snapshot, 1 for a +# broken query, 2 when QRTR or both providers are unavailable, and 3 for +# invalid arguments. qrtr_capture_topology() { qct_output_file="$1" qct_timeout="${2:-${QRTR_LOOKUP_TIMEOUT:-10}}" qct_lookup_bin="${QRTR_LOOKUP_BIN:-qrtr-lookup}" + qct_fallback_bin="${QRTR_LOOKUP_FALLBACK_BIN:-$TOOLS/qrtr_lookup.py}" + QRTR_LOOKUP_PROVIDER="none" + QRTR_LOOKUP_COMMAND="" [ -n "$qct_output_file" ] || return 3 case "$qct_timeout" in @@ -7363,17 +7510,36 @@ qrtr_capture_topology() { esac qrtr_runtime_present || return 2 - command -v "$qct_lookup_bin" >/dev/null 2>&1 || return 2 + if command -v "$qct_lookup_bin" >/dev/null 2>&1; then + QRTR_LOOKUP_PROVIDER="native-qrtr-lookup" + QRTR_LOOKUP_COMMAND=$(command -v "$qct_lookup_bin") + elif command -v python3 >/dev/null 2>&1 && [ -r "$qct_fallback_bin" ]; then + QRTR_LOOKUP_PROVIDER="bundled-python-af-qipcrtr" + QRTR_LOOKUP_COMMAND="$qct_fallback_bin" + else + export QRTR_LOOKUP_PROVIDER QRTR_LOOKUP_COMMAND + return 2 + fi + export QRTR_LOOKUP_PROVIDER QRTR_LOOKUP_COMMAND qct_output_dir=$(dirname "$qct_output_file") mkdir -p "$qct_output_dir" || return 1 rm -f "$qct_output_file" - if ! run_with_timeout_log \ - "$qct_timeout" \ - "$qct_output_file" \ - "$qct_lookup_bin"; then - return 1 + if [ "$QRTR_LOOKUP_PROVIDER" = "native-qrtr-lookup" ]; then + if ! run_with_timeout_log \ + "$qct_timeout" \ + "$qct_output_file" \ + "$QRTR_LOOKUP_COMMAND"; then + return 1 + fi + else + if ! run_with_timeout_log \ + "$((qct_timeout + 2))" \ + "$qct_output_file" \ + python3 "$QRTR_LOOKUP_COMMAND" --timeout "$qct_timeout"; then + return 1 + fi fi if ! awk ' diff --git a/Runner/utils/lib_qrtr.sh b/Runner/utils/lib_qrtr.sh new file mode 100755 index 00000000..1e067b68 --- /dev/null +++ b/Runner/utils/lib_qrtr.sh @@ -0,0 +1,667 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# qrtr_analyze_topology +# Validates qrtr-lookup rows, rejects duplicate endpoint tuples, writes a stable +# TSV representation, and exports bounded topology counts and failure detail. +# Inputs: readable raw topology and two destination paths. Output: no stdout. +# Returns: 0 when valid, 1 on processing failure, 3 for invalid input. +# Side effects: replaces normalized and summary artifacts and exports QRTR_TOPOLOGY_*. +qrtr_analyze_topology() { + qat_raw_file="$1" + qat_normalized_file="$2" + qat_summary_file="$3" + + QRTR_TOPOLOGY_ROW_COUNT=0 + QRTR_TOPOLOGY_SERVICE_COUNT=0 + QRTR_TOPOLOGY_NODE_COUNT=0 + QRTR_TOPOLOGY_FAILURE_REASON="" + + if [ ! -r "$qat_raw_file" ] || + [ -z "$qat_normalized_file" ] || + [ -z "$qat_summary_file" ]; then + QRTR_TOPOLOGY_FAILURE_REASON="invalid-input" + export QRTR_TOPOLOGY_ROW_COUNT QRTR_TOPOLOGY_SERVICE_COUNT + export QRTR_TOPOLOGY_NODE_COUNT QRTR_TOPOLOGY_FAILURE_REASON + return 3 + fi + + mkdir -p "$(dirname "$qat_normalized_file")" || return 1 + rm -f "$qat_normalized_file" "$qat_summary_file" + + awk -v normalized="$qat_normalized_file" -v summary="$qat_summary_file" ' + BEGIN { + OFS="\t" + print "service", "version", "instance", "node", "port" > normalized + } + NR == 1 { + if ($1 != "Service" || $2 != "Version" || $3 != "Instance" || + $4 != "Node" || $5 != "Port") { + reason="invalid-header" + exit 1 + } + next + } + NF == 0 { + next + } + { + for (field=1; field<=5; field++) { + if (field == 2 && $1 == 4097 && $2 == "N/A") { + continue + } + if ($field !~ /^[0-9]+$/) { + reason="non-numeric-field-at-line-" NR + exit 1 + } + } + if (($4 + 0) == 0 || ($5 + 0) == 0) { + reason="zero-node-or-port-at-line-" NR + exit 1 + } + tuple=$1 SUBSEP $2 SUBSEP $3 SUBSEP $4 SUBSEP $5 + if (tuple in tuples) { + reason="duplicate-endpoint-at-line-" NR + exit 1 + } + tuples[tuple]=1 + services[$1 SUBSEP $2 SUBSEP $3]=1 + nodes[$4]=1 + rows++ + print $1, $2, $3, $4, $5 >> normalized + } + END { + if (reason == "" && rows == 0) { + reason="no-service-rows" + } + service_count=0 + node_count=0 + for (key in services) { + service_count++ + } + for (key in nodes) { + node_count++ + } + print "rows=" rows > summary + print "services=" service_count >> summary + print "nodes=" node_count >> summary + print "reason=" reason >> summary + if (reason != "") { + exit 1 + } + } + ' "$qat_raw_file" + qat_rc=$? + + if [ -r "$qat_summary_file" ]; then + QRTR_TOPOLOGY_ROW_COUNT=$( + sed -n 's/^rows=//p' "$qat_summary_file" | sed -n '1p' + ) + QRTR_TOPOLOGY_SERVICE_COUNT=$( + sed -n 's/^services=//p' "$qat_summary_file" | sed -n '1p' + ) + QRTR_TOPOLOGY_NODE_COUNT=$( + sed -n 's/^nodes=//p' "$qat_summary_file" | sed -n '1p' + ) + QRTR_TOPOLOGY_FAILURE_REASON=$( + sed -n 's/^reason=//p' "$qat_summary_file" | sed -n '1p' + ) + fi + + export QRTR_TOPOLOGY_ROW_COUNT QRTR_TOPOLOGY_SERVICE_COUNT + export QRTR_TOPOLOGY_NODE_COUNT QRTR_TOPOLOGY_FAILURE_REASON + return "$qat_rc" +} + +# qrtr_log_runtime_evidence +# Logs the runtime indicators and lookup command used for applicability. +# Inputs: QRTR runtime sysfs, procfs, modules, and optional lookup overrides. +# Output: no machine-readable stdout. Returns: 0. Side effects: emits one log line. +qrtr_log_runtime_evidence() { + qlre_sysfs=absent + qlre_proc=absent + qlre_module=absent + qlre_lookup=not-found + + [ -d /sys/bus/qrtr ] && qlre_sysfs=present + [ -r /proc/net/qrtr ] && qlre_proc=present + if [ -d /sys/module/qrtr ] || is_module_loaded qrtr; then + qlre_module=loaded-or-built-in + fi + qlre_lookup=$(command -v "${QRTR_LOOKUP_BIN:-qrtr-lookup}" 2>/dev/null || true) + qlre_python=$(command -v python3 2>/dev/null || true) + qlre_fallback="${QRTR_LOOKUP_FALLBACK_BIN:-$TOOLS/qrtr_lookup.py}" + qlre_fallback_state=unavailable + if [ -n "$qlre_python" ] && [ -r "$qlre_fallback" ]; then + qlre_fallback_state=available + fi + log_info "[QRTR-RUNTIME] sys_bus=$qlre_sysfs proc_net=$qlre_proc module=$qlre_module native_lookup=${qlre_lookup:-not-found} fallback=$qlre_fallback_state python=${qlre_python:-not-found}" +} + +# qrtr_log_topology [max-rows] [label] +# Replays normalized endpoint tuples as bounded, human-readable live evidence. +# Inputs: normalized TSV, positive row limit, and optional label. Output: logs only. +# Returns: 0 when replayed, 1 when unreadable, 3 for an invalid limit. +qrtr_log_topology() { + qlt_file="$1" + qlt_max_rows="${2:-64}" + qlt_label="${3:-QRTR-SERVICE}" + qlt_total=0 + qlt_emitted=0 + + [ -r "$qlt_file" ] || return 1 + case "$qlt_max_rows" in + ''|*[!0-9]*|0) + return 3 + ;; + esac + + qlt_total=$(awk 'NR > 1 && NF >= 5 { count++ } END { print count + 0 }' "$qlt_file") + while IFS="$(printf '\t')" read -r qlt_service qlt_version qlt_instance qlt_node qlt_port; do + [ "$qlt_service" = "service" ] && continue + [ -n "$qlt_port" ] || continue + if [ "$qlt_emitted" -ge "$qlt_max_rows" ]; then + break + fi + log_info "[$qlt_label] service=$qlt_service version=$qlt_version instance=$qlt_instance node=$qlt_node port=$qlt_port" + qlt_emitted=$((qlt_emitted + 1)) + done <"$qlt_file" + + if [ "$qlt_total" -gt "$qlt_emitted" ]; then + log_info "[$qlt_label] omitted=$((qlt_total - qlt_emitted)) total=$qlt_total artifact=$qlt_file" + fi +} + +# qrtr_log_service_matches [label] +# Logs every endpoint matching one protocol tuple, including its node and port. +# Inputs: normalized topology, decimal tuple fields, and optional label. +# Output: logs only. Returns: 0 when readable, 1 otherwise. Side effects: none. +qrtr_log_service_matches() { + qlsm_file="$1" + qlsm_service="$2" + qlsm_version="$3" + qlsm_instance="$4" + qlsm_label="${5:-QRTR-MATCH}" + + [ -r "$qlsm_file" ] || return 1 + awk -v service="$qlsm_service" -v version="$qlsm_version" \ + -v instance="$qlsm_instance" ' + NR > 1 && $1 == service && $2 == version && $3 == instance { + print $1 "\t" $2 "\t" $3 "\t" $4 "\t" $5 + } + ' "$qlsm_file" | + while IFS="$(printf '\t')" read -r qlsm_s qlsm_v qlsm_i qlsm_node qlsm_port; do + log_info "[$qlsm_label] service=$qlsm_s version=$qlsm_v instance=$qlsm_i node=$qlsm_node port=$qlsm_port" + done +} + +# qrtr_find_service_endpoint +# Prints a unique matching node and port as two tab-separated decimal values. +# Returns 2 when multiple endpoints match so callers do not select by order. +# Inputs: normalized topology and decimal tuple fields. Side effects: none. +# Returns: 0 for one match, 1 for none/unreadable input, 2 for multiple matches. +qrtr_find_service_endpoint() { + qfse_file="$1" + qfse_service="$2" + qfse_version="$3" + qfse_instance="$4" + + [ -r "$qfse_file" ] || return 1 + awk -v service="$qfse_service" -v version="$qfse_version" \ + -v instance="$qfse_instance" ' + NR > 1 && $1 == service && $2 == version && $3 == instance { + node=$4 + port=$5 + count++ + } + END { + if (count == 1) { + print node "\t" port + exit 0 + } + if (count > 1) { + exit 2 + } + exit 1 + } + ' "$qfse_file" +} + +# qrtr_validate_expected_services +# Validates comma-separated service[:version[:instance]] selectors and records +# one stable report row per requested selector. +# Inputs: normalized topology, selector list, and destination report path. +# Output: no stdout. Returns: 0 when present, 1 when missing, 3 for invalid input. +# Side effects: replaces the report and exports QRTR_EXPECTED_* counters/reason. +qrtr_validate_expected_services() { + qves_topology_file="$1" + qves_selectors="$2" + qves_report_file="$3" + qves_list_file="${qves_report_file}.selectors" + + QRTR_EXPECTED_SERVICE_COUNT=0 + QRTR_MISSING_SERVICE_COUNT=0 + QRTR_EXPECTED_FAILURE_REASON="" + + if [ ! -r "$qves_topology_file" ] || [ -z "$qves_report_file" ]; then + QRTR_EXPECTED_FAILURE_REASON="invalid-input" + export QRTR_EXPECTED_SERVICE_COUNT QRTR_MISSING_SERVICE_COUNT + export QRTR_EXPECTED_FAILURE_REASON + return 3 + fi + + : >"$qves_report_file" || return 1 + if [ -z "$qves_selectors" ]; then + rm -f "$qves_list_file" + export QRTR_EXPECTED_SERVICE_COUNT QRTR_MISSING_SERVICE_COUNT + export QRTR_EXPECTED_FAILURE_REASON + return 0 + fi + + printf '%s\n' "$qves_selectors" | tr ',' '\n' >"$qves_list_file" || return 1 + + while IFS= read -r qves_selector; do + if [ -z "$qves_selector" ]; then + QRTR_EXPECTED_FAILURE_REASON="empty-selector" + break + fi + + qves_colon_count=$( + printf '%s' "$qves_selector" | tr -cd ':' | wc -c | tr -d '[:space:]' + ) + case "$qves_colon_count" in + 0|1|2) + ;; + *) + QRTR_EXPECTED_FAILURE_REASON="invalid-selector-$qves_selector" + break + ;; + esac + + qves_service=${qves_selector%%:*} + qves_remainder=${qves_selector#*:} + qves_version="" + qves_instance="" + + if [ "$qves_remainder" != "$qves_selector" ]; then + qves_version=${qves_remainder%%:*} + if [ "${qves_remainder#*:}" != "$qves_remainder" ]; then + qves_instance=${qves_remainder#*:} + fi + fi + + case "$qves_service" in + ''|*[!0-9]*) + QRTR_EXPECTED_FAILURE_REASON="invalid-selector-$qves_selector" + break + ;; + esac + + if [ "$qves_colon_count" -ge 1 ]; then + case "$qves_version" in + ''|*[!0-9]*) + QRTR_EXPECTED_FAILURE_REASON="invalid-selector-$qves_selector" + break + ;; + esac + fi + if [ "$qves_colon_count" -eq 2 ]; then + case "$qves_instance" in + ''|*[!0-9]*) + QRTR_EXPECTED_FAILURE_REASON="invalid-selector-$qves_selector" + break + ;; + esac + fi + + QRTR_EXPECTED_SERVICE_COUNT=$((QRTR_EXPECTED_SERVICE_COUNT + 1)) + if qrtr_topology_has_service \ + "$qves_topology_file" \ + "$qves_service" \ + "$qves_version" \ + "$qves_instance"; then + printf '%s\tpresent\n' "$qves_selector" >>"$qves_report_file" + else + QRTR_MISSING_SERVICE_COUNT=$((QRTR_MISSING_SERVICE_COUNT + 1)) + printf '%s\tmissing\n' "$qves_selector" >>"$qves_report_file" + fi + done <"$qves_list_file" + + rm -f "$qves_list_file" + export QRTR_EXPECTED_SERVICE_COUNT QRTR_MISSING_SERVICE_COUNT + export QRTR_EXPECTED_FAILURE_REASON + + if [ -n "$QRTR_EXPECTED_FAILURE_REASON" ]; then + return 3 + fi + + [ "$QRTR_MISSING_SERVICE_COUNT" -eq 0 ] +} + +# pd_mapper_capture_kernel_runtime +# Captures kernel PD Mapper configuration, driver registration, module state, +# and each runtime auxiliary device without assuming a SoC-specific instance. +# Input: destination TSV path. Output: no stdout. +# Returns: 0 on capture, 1 on artifact failure, 3 for invalid input. +# Side effects: replaces the report and exports PD_MAPPER_* runtime counters. +pd_mapper_capture_kernel_runtime() { + pmckr_report_file="$1" + + [ -n "$pmckr_report_file" ] || return 3 + : >"$pmckr_report_file" || return 1 + + PD_MAPPER_KERNEL_CONFIG="unknown" + PD_MAPPER_MODULE_STATE="not-exposed" + PD_MAPPER_DRIVER_STATE="not-registered" + PD_MAPPER_REGISTERED_DRIVER="none" + PD_MAPPER_AUX_COUNT=0 + PD_MAPPER_BOUND_COUNT=0 + PD_MAPPER_UNBOUND_COUNT=0 + PD_MAPPER_WRONG_DRIVER_COUNT=0 + + pmckr_config_line=$(kernel_config_value CONFIG_QCOM_PD_MAPPER 2>/dev/null || true) + if [ -n "$pmckr_config_line" ]; then + PD_MAPPER_KERNEL_CONFIG=${pmckr_config_line#CONFIG_QCOM_PD_MAPPER=} + fi + if [ -d /sys/module/qcom_pd_mapper ]; then + PD_MAPPER_MODULE_STATE="loaded" + elif [ "$PD_MAPPER_KERNEL_CONFIG" = "y" ]; then + PD_MAPPER_MODULE_STATE="built-in-or-not-instantiated" + fi + for pmckr_driver_path in \ + /sys/bus/auxiliary/drivers/qcom-pdm-mapper \ + /sys/bus/auxiliary/drivers/*.qcom-pdm-mapper; do + [ -d "$pmckr_driver_path" ] || continue + PD_MAPPER_DRIVER_STATE="registered" + PD_MAPPER_REGISTERED_DRIVER=${pmckr_driver_path##*/} + break + done + + printf 'kind\tname\tdriver\tparent\tstate\n' >"$pmckr_report_file" + for pmckr_device in /sys/bus/auxiliary/devices/qcom_common.pd-mapper.*; do + [ -e "$pmckr_device" ] || continue + PD_MAPPER_AUX_COUNT=$((PD_MAPPER_AUX_COUNT + 1)) + pmckr_name=${pmckr_device##*/} + pmckr_resolved=$(readlink -f "$pmckr_device" 2>/dev/null || true) + pmckr_parent=$(dirname "${pmckr_resolved:-$pmckr_device}") + pmckr_driver="unbound" + pmckr_state="unbound" + if [ -L "$pmckr_device/driver" ]; then + pmckr_driver=$(basename "$(readlink -f "$pmckr_device/driver")") + pmckr_state="bound" + PD_MAPPER_BOUND_COUNT=$((PD_MAPPER_BOUND_COUNT + 1)) + case "$pmckr_driver" in + qcom-pdm-mapper|*.qcom-pdm-mapper) + ;; + *) + pmckr_state="wrong-driver" + PD_MAPPER_WRONG_DRIVER_COUNT=$((PD_MAPPER_WRONG_DRIVER_COUNT + 1)) + ;; + esac + else + PD_MAPPER_UNBOUND_COUNT=$((PD_MAPPER_UNBOUND_COUNT + 1)) + fi + printf 'aux\t%s\t%s\t%s\t%s\n' \ + "$pmckr_name" \ + "$pmckr_driver" \ + "$pmckr_parent" \ + "$pmckr_state" >>"$pmckr_report_file" + done + + export PD_MAPPER_KERNEL_CONFIG PD_MAPPER_MODULE_STATE + export PD_MAPPER_REGISTERED_DRIVER + export PD_MAPPER_DRIVER_STATE PD_MAPPER_AUX_COUNT + export PD_MAPPER_BOUND_COUNT PD_MAPPER_UNBOUND_COUNT + export PD_MAPPER_WRONG_DRIVER_COUNT +} + +# pd_mapper_log_kernel_runtime [max-devices] +# Replays bounded auxiliary-device binding evidence to the live log. +# Inputs: PD Mapper TSV and optional device limit. Output: logs only. +# Returns: 0 when readable, 1 otherwise. Side effects: none. +pd_mapper_log_kernel_runtime() { + pmlkr_report_file="$1" + pmlkr_max_devices="${2:-32}" + pmlkr_emitted=0 + + [ -r "$pmlkr_report_file" ] || return 1 + log_info "[PD-MAPPER-KERNEL] config=$PD_MAPPER_KERNEL_CONFIG module=$PD_MAPPER_MODULE_STATE driver_state=$PD_MAPPER_DRIVER_STATE registered_driver=$PD_MAPPER_REGISTERED_DRIVER auxiliary_devices=$PD_MAPPER_AUX_COUNT bound=$PD_MAPPER_BOUND_COUNT unbound=$PD_MAPPER_UNBOUND_COUNT wrong_driver=$PD_MAPPER_WRONG_DRIVER_COUNT artifact=$pmlkr_report_file" + while IFS="$(printf '\t')" read -r pmlkr_kind pmlkr_name pmlkr_driver pmlkr_parent pmlkr_state; do + [ "$pmlkr_kind" = "kind" ] && continue + if [ "$pmlkr_emitted" -ge "$pmlkr_max_devices" ]; then + break + fi + log_info "[PD-MAPPER-AUX] device=$pmlkr_name driver=$pmlkr_driver parent=$pmlkr_parent state=$pmlkr_state" + pmlkr_emitted=$((pmlkr_emitted + 1)) + done <"$pmlkr_report_file" + if [ "$PD_MAPPER_AUX_COUNT" -gt "$pmlkr_emitted" ]; then + log_info "[PD-MAPPER-AUX] omitted=$((PD_MAPPER_AUX_COUNT - pmlkr_emitted)) total=$PD_MAPPER_AUX_COUNT artifact=$pmlkr_report_file" + fi +} + +# runtime_process_pids +# Prints a space-separated PID list for exact executable names. +# Input: exact process name. Output: PID list on stdout. +# Returns: provider status, or 2 for invalid input/no supported process tool. +# Side effects: none. +runtime_process_pids() { + rpp_name="$1" + + [ -n "$rpp_name" ] || return 2 + + if command -v pidof >/dev/null 2>&1; then + pidof "$rpp_name" 2>/dev/null + return $? + fi + + if command -v pgrep >/dev/null 2>&1; then + pgrep -x "$rpp_name" 2>/dev/null | tr '\n' ' ' | sed 's/[[:space:]]*$//' + return $? + fi + + return 2 +} + +# qrtr_service_discover +# Exports read-only provisioning and runtime state for a QRTR userspace service. +# Inputs: optional systemd unit, exact process name, and executable name. +# Output: no stdout. Returns: 0. +# Side effects: exports QRTR_SERVICE_* applicability and runtime fields. +qrtr_service_discover() { + qsd_unit="$1" + qsd_process="$2" + qsd_binary="$3" + + QRTR_SERVICE_UNIT_EXISTS=0 + QRTR_SERVICE_ACTIVE=0 + QRTR_SERVICE_PIDS="" + QRTR_SERVICE_BINARY_PATH="" + QRTR_SERVICE_APPLICABLE=0 + + if [ -n "$qsd_unit" ] && systemd_service_exists "$qsd_unit"; then + QRTR_SERVICE_UNIT_EXISTS=1 + QRTR_SERVICE_APPLICABLE=1 + if systemd_service_is_active "$qsd_unit"; then + QRTR_SERVICE_ACTIVE=1 + fi + fi + + QRTR_SERVICE_PIDS=$(runtime_process_pids "$qsd_process" 2>/dev/null || true) + if [ -n "$QRTR_SERVICE_PIDS" ]; then + QRTR_SERVICE_ACTIVE=1 + QRTR_SERVICE_APPLICABLE=1 + fi + + QRTR_SERVICE_BINARY_PATH=$(command -v "$qsd_binary" 2>/dev/null || true) + + export QRTR_SERVICE_UNIT_EXISTS QRTR_SERVICE_ACTIVE QRTR_SERVICE_PIDS + export QRTR_SERVICE_BINARY_PATH QRTR_SERVICE_APPLICABLE +} + +# pd_mapper_capture_registry_files +# Lists service-registry files from the firmware directories selected by the +# running remoteproc instances. It does not recursively scan all firmware. +# Input: destination list path. Output: no stdout. +# Returns: 0 on capture, 1 on artifact failure, 3 for invalid input. +# Side effects: replaces and sorts the retained file list. +pd_mapper_capture_registry_files() { + pmcrf_output_file="$1" + pmcrf_firmware_override="" + + [ -n "$pmcrf_output_file" ] || return 3 + : >"$pmcrf_output_file" || return 1 + + if [ -r /sys/module/firmware_class/parameters/path ]; then + pmcrf_firmware_override=$( + sed -n '1p' /sys/module/firmware_class/parameters/path 2>/dev/null + ) + fi + + for pmcrf_remoteproc in /sys/class/remoteproc/remoteproc*; do + [ -r "$pmcrf_remoteproc/firmware" ] || continue + pmcrf_firmware=$(sed -n '1p' "$pmcrf_remoteproc/firmware" 2>/dev/null) + [ -n "$pmcrf_firmware" ] || continue + pmcrf_relative_dir=${pmcrf_firmware%/*} + if [ "$pmcrf_relative_dir" = "$pmcrf_firmware" ]; then + pmcrf_relative_dir="" + fi + + for pmcrf_root in "$pmcrf_firmware_override" /lib/firmware /vendor/firmware; do + [ -n "$pmcrf_root" ] || continue + pmcrf_directory="$pmcrf_root" + if [ -n "$pmcrf_relative_dir" ]; then + pmcrf_directory="$pmcrf_root/$pmcrf_relative_dir" + fi + [ -d "$pmcrf_directory" ] || continue + + find "$pmcrf_directory" -maxdepth 1 -type f \ + \( -name '*.jsn' -o -name '*.jsn.xz' \) \ + -print 2>/dev/null >>"$pmcrf_output_file" + done + done + + if [ -s "$pmcrf_output_file" ]; then + sort -u "$pmcrf_output_file" -o "$pmcrf_output_file" + fi +} + +# pd_mapper_validate_registry_files [timeout] +# Uses image-provided Python when available to parse plain or xz-compressed +# service-registry JSON files. Missing Python leaves the files informational. +# Inputs: registry list, report destination, and positive per-file timeout. +# Output: no stdout. Returns: 0 when no invalid files are found, 1 otherwise, +# or 3 for invalid input. Exports PD_MAPPER_REGISTRY_* counters and validator. +pd_mapper_validate_registry_files() { + pmvrf_list_file="$1" + pmvrf_report_file="$2" + pmvrf_timeout="${3:-5}" + + PD_MAPPER_REGISTRY_COUNT=0 + PD_MAPPER_REGISTRY_VALIDATED_COUNT=0 + PD_MAPPER_REGISTRY_INVALID_COUNT=0 + PD_MAPPER_REGISTRY_VALIDATOR="unavailable" + + [ -r "$pmvrf_list_file" ] && [ -n "$pmvrf_report_file" ] || return 3 + : >"$pmvrf_report_file" || return 1 + + if command -v python3 >/dev/null 2>&1; then + PD_MAPPER_REGISTRY_VALIDATOR="python3" + fi + + while IFS= read -r pmvrf_file; do + [ -n "$pmvrf_file" ] || continue + PD_MAPPER_REGISTRY_COUNT=$((PD_MAPPER_REGISTRY_COUNT + 1)) + + if [ "$PD_MAPPER_REGISTRY_VALIDATOR" = "unavailable" ]; then + printf '%s\tnot-validated\n' "$pmvrf_file" >>"$pmvrf_report_file" + continue + fi + + if run_with_timeout "$pmvrf_timeout" python3 -c ' +import json, lzma, pathlib, sys +path = pathlib.Path(sys.argv[1]) +opener = lzma.open if path.name.endswith(".xz") else open +with opener(path, "rt", encoding="utf-8") as stream: + root = json.load(stream) +if not isinstance(root, dict): + raise ValueError("root is not an object") +domain = root.get("sr_domain") +services = root.get("sr_service") +if not isinstance(domain, dict) or not isinstance(services, list): + raise ValueError("sr_domain or sr_service has the wrong type") +for key in ("soc", "domain", "subdomain"): + if not isinstance(domain.get(key), str) or not domain[key]: + raise ValueError("sr_domain.%s is missing or invalid" % key) +instance = domain.get("qmi_instance_id") +if not isinstance(instance, (int, float)) or isinstance(instance, bool): + raise ValueError("sr_domain.qmi_instance_id is missing or invalid") +for entry in services: + if not isinstance(entry, dict): + raise ValueError("sr_service entry is not an object") + for key in ("provider", "service"): + if not isinstance(entry.get(key), str) or not entry[key]: + raise ValueError("sr_service.%s is missing or invalid" % key) +' "$pmvrf_file" >/dev/null 2>&1; then + PD_MAPPER_REGISTRY_VALIDATED_COUNT=$((PD_MAPPER_REGISTRY_VALIDATED_COUNT + 1)) + printf '%s\tvalid\n' "$pmvrf_file" >>"$pmvrf_report_file" + else + PD_MAPPER_REGISTRY_INVALID_COUNT=$((PD_MAPPER_REGISTRY_INVALID_COUNT + 1)) + printf '%s\tinvalid\n' "$pmvrf_file" >>"$pmvrf_report_file" + fi + done <"$pmvrf_list_file" + + export PD_MAPPER_REGISTRY_COUNT PD_MAPPER_REGISTRY_VALIDATED_COUNT + export PD_MAPPER_REGISTRY_INVALID_COUNT PD_MAPPER_REGISTRY_VALIDATOR + + [ "$PD_MAPPER_REGISTRY_INVALID_COUNT" -eq 0 ] +} + +# qrtr_capture_service_evidence [timeout] +# Captures bounded systemd and process evidence without changing service state. +# Inputs: optional unit, exact process name, result directory, and timeout seconds. +# Output: no stdout. Returns: 0 after capture, 1 if artifacts cannot be created. +# Side effects: replaces service evidence and exports QCSE_STATUS_RC/QCSE_JOURNAL_RC. +qrtr_capture_service_evidence() { + qcse_unit="$1" + qcse_process="$2" + qcse_result_dir="$3" + qcse_timeout="${4:-10}" + qcse_process_file="$qcse_result_dir/process.log" + + mkdir -p "$qcse_result_dir" || return 1 + : >"$qcse_process_file" || return 1 + + if command -v ps >/dev/null 2>&1; then + ps 2>&1 | awk -v name="$qcse_process" ' + NR == 1 || $0 ~ "(^|[ /])" name "([[:space:]]|$)" { print } + ' >"$qcse_process_file" + fi + + if [ -n "$qcse_unit" ] && systemd_service_exists "$qcse_unit"; then + run_with_timeout_log \ + "$qcse_timeout" \ + "$qcse_result_dir/systemd-status.log" \ + systemctl --no-pager --full --lines=20 status "$qcse_unit" + QCSE_STATUS_RC=$? + + if command -v journalctl >/dev/null 2>&1; then + run_with_timeout_log \ + "$qcse_timeout" \ + "$qcse_result_dir/journal.log" \ + journalctl --no-pager -q -b -u "$qcse_unit" -n 200 + QCSE_JOURNAL_RC=$? + else + QCSE_JOURNAL_RC=127 + : >"$qcse_result_dir/journal.log" + fi + else + QCSE_STATUS_RC=127 + QCSE_JOURNAL_RC=127 + : >"$qcse_result_dir/systemd-status.log" + : >"$qcse_result_dir/journal.log" + fi + + export QCSE_STATUS_RC QCSE_JOURNAL_RC + return 0 +} diff --git a/Runner/utils/qrtr_lookup.py b/Runner/utils/qrtr_lookup.py new file mode 100755 index 00000000..7cedd57d --- /dev/null +++ b/Runner/utils/qrtr_lookup.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +"""Bounded QRTR control-port service lookup using the public Linux ABI.""" + +import argparse +import socket +import struct +import sys + + +AF_QIPCRTR = getattr(socket, "AF_QIPCRTR", 42) +QRTR_PORT_CTRL = 0xFFFFFFFE +QRTR_TYPE_NEW_SERVER = 4 +QRTR_TYPE_NEW_LOOKUP = 10 +QRTR_DIAG_SERVICE = 4097 +CTRL_PACKET = struct.Struct("> 8 + print("{} {} {} {} {}".format(service, version, instance, node, port)) + + +if __name__ == "__main__": + sys.exit(main()) From 4b95ec16cdd3e2ed524e62e77a645c4119c31913 Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Fri, 18 Sep 2026 13:32:37 +0530 Subject: [PATCH 2/4] tqftp: add exact QRTR transfer validation Discover tqftpserv and its QRTR endpoint dynamically, then stage a temporary payload and verify an exact bounded RRQ transfer over AF_QIPCRTR. Retain service, topology, packet, payload, and kernel evidence while restoring only test-created state. Signed-off-by: Srikanth Muppandam --- .../Baseport/TQFTP_Validation/README.md | 80 ++++ .../TQFTP_Validation/TQFTP_Validation.yaml | 20 + .../Kernel/Baseport/TQFTP_Validation/run.sh | 366 ++++++++++++++++++ Runner/utils/tqftp_client.py | 227 +++++++++++ 4 files changed, 693 insertions(+) create mode 100644 Runner/suites/Kernel/Baseport/TQFTP_Validation/README.md create mode 100755 Runner/suites/Kernel/Baseport/TQFTP_Validation/TQFTP_Validation.yaml create mode 100755 Runner/suites/Kernel/Baseport/TQFTP_Validation/run.sh create mode 100755 Runner/utils/tqftp_client.py diff --git a/Runner/suites/Kernel/Baseport/TQFTP_Validation/README.md b/Runner/suites/Kernel/Baseport/TQFTP_Validation/README.md new file mode 100644 index 00000000..0ec779c9 --- /dev/null +++ b/Runner/suites/Kernel/Baseport/TQFTP_Validation/README.md @@ -0,0 +1,80 @@ +# TQFTP validation + +`TQFTP_Validation` performs readiness and exact local-host transfer validation +of the public QRTR TFTP server. Applicability comes from the +`tqftpserv.service` unit or a running `tqftpserv` process. An image-provided binary is retained as diagnostic +evidence but does not by itself prove that the service applies. + +The service set still varies by target, but users do not provide a QRTR service +number. Run `./run.sh` and the suite dynamically decides whether TQFTP applies, +then checks its public protocol tuple `4096:1:0`. The tuple is defined by +`tqftpserv`; it is not inferred from a particular board. + +The active server must advertise QRTR service `4096`, version `1`, instance +`0`. The suite checks access to the state directory and searches a bounded +service journal for public request and transfer markers. + +By default, it also stages a temporary deterministic payload in the read-write +directory and uses the repository Python client to issue an RRQ to the +dynamically discovered local `4096:1:0` QRTR endpoint. The received bytes must +exactly match the staged file. This covers the QRTR socket, request/response, +path translation, TQFTP `blksize`/`wsize`/`rsize` option negotiation, OACK, +multi-block transfer, acknowledgments, and payload integrity. +The temporary source file is removed on success, failure, timeout, and signal. + +The E2E path uses local `AF_QIPCRTR` traffic. It does not require Wi-Fi, +Ethernet, DNS, or Internet access, and an IP-network transfer cannot substitute +for this protocol check. The client uses Python's named `AF_QIPCRTR` constant +when available and the public Linux family number otherwise. Socket failures +remain explicit functional diagnostics. +If more than one endpoint advertises the same tuple, the E2E subcheck prints +all candidates and skips instead of choosing an endpoint by enumeration order. + +## Run + +```sh +./run.sh +./run.sh --timeout 15 --state-dir /var/lib/tqftpserv +./run.sh --e2e 0 +``` + +CLI options override the matching environment variables: + +| Option | Environment | Default | Purpose | +|---|---|---:|---| +| `--timeout` | `TQFTP_TIMEOUT` | `10` | Bound QRTR and service evidence commands | +| `--state-dir` | `TQFTP_STATE_DIR` | `/var/lib/tqftpserv` | Server read-write root and E2E staging path | +| `--e2e` | `TQFTP_E2E_ENABLE` | `1` | Enable the local exact RRQ transfer | + +The default state directory follows the public `tqftpserv` implementation and +systemd `StateDirectory=tqftpserv` unit. Override it only for an image carrying +a deliberately modified TQFTP build or service definition. `--timeout` only +changes probe bounds. Neither option is normally SoC-specific. + +## Result policy + +- `PASS`: TQFTP is active, advertises `4096:1:0`, exposed state is usable, and + an enabled E2E transfer returns the exact staged payload. +- `FAIL`: TQFTP is provisioned but inactive, advertisement is absent, a query + or enabled E2E transfer fails, state access is invalid, or relevant kernel + errors are present. +- `SKIP`: TQFTP is not provisioned. No request in the bounded journal is a + subcheck skip because remote firmware may make no request during the run. + +Look for `[TQFTP-DISCOVERY]`, `[TQFTP-QRTR]`, `[TQFTP-QRTR-ENDPOINT]`, +`[TQFTP-E2E]`, `[TQFTP-STATE]`, `[TQFTP-STATE-ENTRY]`, and +`[TQFTP-REQUEST]` in stdout. +Endpoint lines include the serving node and port. State, request, service, and +kernel-error excerpts are bounded and point to the complete retained artifact. +The E2E artifact includes the negotiated OACK values, each data block up to a +bounded display limit, cumulative byte counts, and the final SHA-256 proof. +When no request occurred, the request marker reports the journal command status +and number of retained lines so readiness is distinguishable from traffic proof. +`[TQFTP-POLICY]` records the dynamically selected +applicability contract and effective state directory. Service, topology, +request, E2E client output, the received payload, and kernel evidence is retained under the printed +`results/TQFTP_Validation/run-*/` directory. + +Kernel health capture prefers `dmesg` and falls back to image-provided +`journalctl -k`. `kernel/dmesg_access.log` records the selected provider, +command status, `dmesg_restrict`, effective capabilities, and any access error. diff --git a/Runner/suites/Kernel/Baseport/TQFTP_Validation/TQFTP_Validation.yaml b/Runner/suites/Kernel/Baseport/TQFTP_Validation/TQFTP_Validation.yaml new file mode 100755 index 00000000..b356e258 --- /dev/null +++ b/Runner/suites/Kernel/Baseport/TQFTP_Validation/TQFTP_Validation.yaml @@ -0,0 +1,20 @@ +metadata: + name: TQFTP_Validation + format: "Lava-Test Test Definition 1.0" + description: "Dynamically validate TQFTP readiness, QRTR advertisement, and an exact local-host QRTR transfer" + os: + - linux + scope: + - functional + +params: + TQFTP_TIMEOUT: "10" + TQFTP_STATE_DIR: "/var/lib/tqftpserv" + TQFTP_E2E_ENABLE: "1" + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Kernel/Baseport/TQFTP_Validation + - ./run.sh --timeout "${TQFTP_TIMEOUT}" --state-dir "${TQFTP_STATE_DIR}" --e2e "${TQFTP_E2E_ENABLE}" || true + - $REPO_PATH/Runner/utils/send-to-lava.sh TQFTP_Validation.res diff --git a/Runner/suites/Kernel/Baseport/TQFTP_Validation/run.sh b/Runner/suites/Kernel/Baseport/TQFTP_Validation/run.sh new file mode 100755 index 00000000..684b2278 --- /dev/null +++ b/Runner/suites/Kernel/Baseport/TQFTP_Validation/run.sh @@ -0,0 +1,366 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# ---------- Repo env + helpers ---------- +SCRIPT_DIR="$( + cd "$(dirname "$0")" || exit 1 + pwd +)" +INIT_ENV="" +SEARCH="$SCRIPT_DIR" + +while [ "$SEARCH" != "/" ]; do + if [ -f "$SEARCH/init_env" ]; then + INIT_ENV="$SEARCH/init_env" + break + fi + SEARCH=$(dirname "$SEARCH") +done + +if [ -z "$INIT_ENV" ]; then + echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 + exit 1 +fi + +# Only source once (idempotent) +# NOTE: We intentionally **do not export** any new vars. They stay local to this shell. +if [ -z "${__INIT_ENV_LOADED:-}" ]; then + # shellcheck disable=SC1090 + . "$INIT_ENV" + __INIT_ENV_LOADED=1 +fi + +# shellcheck disable=SC1090 +. "$INIT_ENV" +# shellcheck disable=SC1091 +. "$TOOLS/functestlib.sh" +# shellcheck disable=SC1091 +. "$TOOLS/lib_qrtr.sh" +TESTNAME="TQFTP_Validation" +RES_FILE="$SCRIPT_DIR/$TESTNAME.res" + +TQFTP_TIMEOUT="${TQFTP_TIMEOUT:-10}" +TQFTP_STATE_DIR="${TQFTP_STATE_DIR:-/var/lib/tqftpserv}" +TQFTP_E2E_ENABLE="${TQFTP_E2E_ENABLE:-1}" +RESULT_DIR="$SCRIPT_DIR/results/$TESTNAME/run-$(date '+%Y%m%d-%H%M%S')-$$" +TOPOLOGY_FILE="$RESULT_DIR/qrtr_lookup.log" +REQUEST_REPORT="$RESULT_DIR/request_markers.log" +TQFTP_CORE_UNVERIFIED=0 +TQFTP_TEST_SOURCE="" + +# cleanup +# Remove only the temporary TQFTP source created by this run and close stdout capture. +# Inputs: trap status and TQFTP_TEST_SOURCE. Output: logs only. +# Returns: exits through _runner_stdout_cleanup. Side effects: removes the staged file. +cleanup() { + cleanup_status=$? + if [ -n "$TQFTP_TEST_SOURCE" ] && [ -f "$TQFTP_TEST_SOURCE" ]; then + log_warn "[TQFTP-E2E] phase=cleanup action=remove-temporary-source path=$TQFTP_TEST_SOURCE trigger=exit-or-signal" + rm -f "$TQFTP_TEST_SOURCE" || true + fi + [ "$cleanup_status" -eq 0 ] + _runner_stdout_cleanup +} + +# usage +# Print TQFTP CLI options, automatic discovery behavior, and precedence. +# Inputs: none. Output: help text on stdout. Returns: 0. Side effects: none. +usage() { + printf '%s\n' \ + "Usage: ./run.sh [options]" \ + " --timeout SECONDS Bound runtime probes, default: 10" \ + " --state-dir PATH Override a custom TQFTP read-write directory" \ + " --e2e 0|1 Local QRTR read-transfer validation, default: 1" \ + " -h, --help" \ + "Service applicability and QRTR endpoints are discovered automatically." \ + "CLI options override environment variables." +} + +# parse_args ARG... +# Parse CLI overrides into TQFTP timeout, state-directory, and E2E policy globals. +# Inputs: command-line arguments. Output: no stdout. +# Returns: 0 on success, 2 for invalid input, or exits after help. Side effects: globals. +parse_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + --timeout) + [ "$#" -ge 2 ] || return 2 + TQFTP_TIMEOUT="$2" + shift 2 + ;; + --state-dir) + [ "$#" -ge 2 ] || return 2 + TQFTP_STATE_DIR="$2" + shift 2 + ;; + --e2e) + [ "$#" -ge 2 ] || return 2 + TQFTP_E2E_ENABLE="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + log_error "Unknown argument: $1" + return 2 + ;; + esac + done +} + +parse_args "$@" || { + usage >&2 + exit 2 +} + +test_result_init "$TESTNAME" "$RES_FILE" +trap cleanup EXIT HUP INT TERM +if ! mkdir -p "$RESULT_DIR"; then + test_result_finish "FAIL" "$TESTNAME FAIL: cannot create retained evidence directory $RESULT_DIR" +fi + +log_info "--------------------------------------------------------------------------" +log_info "Starting $TESTNAME" +log_info "Evidence directory: $RESULT_DIR" +log_info "TQFTP validation: checking server readiness and an optional exact local-host file transfer over QRTR service 4096:1:0" +log_info "[TQFTP-POLICY] applicability=dynamic service_tuple=4096:1:0 source=public-runtime-contract state_dir=$TQFTP_STATE_DIR timeout=${TQFTP_TIMEOUT}s e2e=$TQFTP_E2E_ENABLE transport=local-AF_QIPCRTR" + +case "$TQFTP_TIMEOUT" in + ''|*[!0-9]*|0) + test_result_record "FAIL" "TQFTP configuration is invalid, timeout must be a positive integer" + test_result_finish + ;; +esac + +case "$TQFTP_E2E_ENABLE" in + 0|1) + ;; + *) + test_result_record "FAIL" "TQFTP E2E enable must be 0 or 1, observed=$TQFTP_E2E_ENABLE" + test_result_finish + ;; +esac + +case "$TQFTP_STATE_DIR" in + /*) + ;; + *) + test_result_record "FAIL" "TQFTP state directory must be an absolute path, observed=$TQFTP_STATE_DIR" + test_result_finish + ;; +esac + +qrtr_service_discover tqftpserv.service tqftpserv tqftpserv +log_info "[TQFTP-DISCOVERY] unit=tqftpserv.service unit_exists=$QRTR_SERVICE_UNIT_EXISTS active=$QRTR_SERVICE_ACTIVE pids=${QRTR_SERVICE_PIDS:-none} binary=${QRTR_SERVICE_BINARY_PATH:-not-found} state_dir=$TQFTP_STATE_DIR" + +if [ "$QRTR_SERVICE_APPLICABLE" -eq 0 ]; then + test_result_record "SKIP" "TQFTP has no installed service unit or running process, binary=${QRTR_SERVICE_BINARY_PATH:-not-found}" + test_result_finish +fi + +qrtr_capture_service_evidence \ + tqftpserv.service \ + tqftpserv \ + "$RESULT_DIR/service" \ + "$TQFTP_TIMEOUT" + +if [ "$QRTR_SERVICE_ACTIVE" -ne 1 ]; then + log_file_with_label "TQFTP-SERVICE-STATUS" "$RESULT_DIR/service/systemd-status.log" 25 + log_file_with_label "TQFTP-PROCESS" "$RESULT_DIR/service/process.log" 10 + test_result_record "FAIL" "TQFTP is provisioned but not active, unit_exists=$QRTR_SERVICE_UNIT_EXISTS binary=${QRTR_SERVICE_BINARY_PATH:-not-found} status_artifact=$RESULT_DIR/service/systemd-status.log" +else + test_result_record "PASS" "TQFTP runtime is active, pids=${QRTR_SERVICE_PIDS:-systemd-confirmed}" +fi + +if [ "$QRTR_SERVICE_ACTIVE" -eq 1 ]; then + qrtr_capture_topology "$TOPOLOGY_FILE" "$TQFTP_TIMEOUT" + topology_rc=$? + case "$topology_rc" in + 0) + log_info "[TQFTP-QRTR] lookup_provider=$QRTR_LOOKUP_PROVIDER command=$QRTR_LOOKUP_COMMAND artifact=$TOPOLOGY_FILE" + if qrtr_topology_has_service "$TOPOLOGY_FILE" 4096 1 0; then + log_info "[TQFTP-QRTR] expected=4096:1:0 observed=present artifact=$TOPOLOGY_FILE" + qrtr_log_service_matches "$TOPOLOGY_FILE" 4096 1 0 "TQFTP-QRTR-ENDPOINT" + test_result_record "PASS" "TQFTP advertises QRTR service 4096 version 1 instance 0" + else + log_fail "[TQFTP-QRTR] expected=4096:1:0 observed=missing artifact=$TOPOLOGY_FILE" + log_file_with_label "TQFTP-QRTR-RAW" "$TOPOLOGY_FILE" 25 + test_result_record "FAIL" "Active TQFTP does not advertise QRTR service 4096 version 1 instance 0" + fi + ;; + 2) + test_result_record "SKIP" "TQFTP is active but neither QRTR lookup provider nor QRTR runtime evidence is available" + TQFTP_CORE_UNVERIFIED=1 + ;; + *) + log_file_with_label "TQFTP-QRTR-RAW" "$TOPOLOGY_FILE" 25 + test_result_record "FAIL" "TQFTP QRTR topology query failed, rc=$topology_rc artifact=$TOPOLOGY_FILE" + ;; + esac +fi + +if [ -d "$TQFTP_STATE_DIR" ]; then + if [ -r "$TQFTP_STATE_DIR" ] && [ -w "$TQFTP_STATE_DIR" ]; then + state_listing="$RESULT_DIR/state_directory.log" + ls -la "$TQFTP_STATE_DIR" >"$state_listing" 2>&1 + state_entries=$(find "$TQFTP_STATE_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l | tr -d '[:space:]') + log_info "[TQFTP-STATE] path=$TQFTP_STATE_DIR access=read-write entries=${state_entries:-unknown} artifact=$state_listing" + log_file_with_label "TQFTP-STATE-ENTRY" "$state_listing" 20 + test_result_record "PASS" "TQFTP state directory is accessible at $TQFTP_STATE_DIR" + else + log_fail "[TQFTP-STATE] path=$TQFTP_STATE_DIR access=insufficient" + test_result_record "FAIL" "TQFTP state directory exists but is not readable and writable by the test user" + fi +else + test_result_record "SKIP" "TQFTP state directory is not present at $TQFTP_STATE_DIR" +fi + +if [ "$TQFTP_E2E_ENABLE" = "1" ] && + [ "$QRTR_SERVICE_ACTIVE" -eq 1 ] && + [ "${topology_rc:-1}" -eq 0 ] && + qrtr_topology_has_service "$TOPOLOGY_FILE" 4096 1 0; then + if ! command -v python3 >/dev/null 2>&1; then + test_result_record "SKIP" "TQFTP E2E requires image-provided Python" + elif [ ! -d "$TQFTP_STATE_DIR" ] || [ ! -w "$TQFTP_STATE_DIR" ]; then + test_result_record "SKIP" "TQFTP E2E cannot stage its temporary read-only payload in $TQFTP_STATE_DIR" + else + endpoint=$(qrtr_find_service_endpoint "$TOPOLOGY_FILE" 4096 1 0 2>/dev/null) + endpoint_rc=$? + if [ "$endpoint_rc" -eq 2 ]; then + log_info "[TQFTP-E2E] phase=selection action=skip reason=multiple-service-endpoints" + qrtr_log_service_matches "$TOPOLOGY_FILE" 4096 1 0 "TQFTP-E2E-CANDIDATE" + test_result_record "SKIP" "Multiple TQFTP service endpoints are advertised, local server ownership cannot be selected safely by enumeration order" + endpoint="" + elif [ "$endpoint_rc" -ne 0 ]; then + test_result_record "FAIL" "TQFTP service endpoint could not be resolved from the validated topology, rc=$endpoint_rc" + endpoint="" + fi + e2e_node=$(printf '%s\n' "$endpoint" | awk '{ print $1 }') + e2e_port=$(printf '%s\n' "$endpoint" | awk '{ print $2 }') + e2e_received="$RESULT_DIR/tqftp_e2e_received.bin" + e2e_log="$RESULT_DIR/tqftp_e2e.log" + e2e_ready=1 + if [ "$endpoint_rc" -ne 0 ]; then + e2e_ready=0 + fi + if [ "$e2e_ready" -eq 1 ]; then + case "$e2e_node" in + ''|*[!0-9]*) + test_result_record "FAIL" "TQFTP endpoint discovery returned an invalid node or port, observed=${endpoint:-empty}" + e2e_ready=0 + ;; + esac + case "$e2e_port" in + ''|*[!0-9]*) + if [ "$e2e_ready" -eq 1 ]; then + test_result_record "FAIL" "TQFTP endpoint discovery returned an invalid node or port, observed=${endpoint:-empty}" + fi + e2e_ready=0 + ;; + esac + fi + if [ "$e2e_ready" -eq 1 ]; then + if ! command -v mktemp >/dev/null 2>&1; then + test_result_record "SKIP" "TQFTP E2E cannot stage a collision-safe temporary payload because mktemp is unavailable" + e2e_ready=0 + else + TQFTP_TEST_SOURCE=$(mktemp "$TQFTP_STATE_DIR/qli_tqftp_e2e.XXXXXX" 2>/dev/null || true) + if [ -z "$TQFTP_TEST_SOURCE" ]; then + test_result_record "FAIL" "TQFTP E2E could not create a temporary source payload in $TQFTP_STATE_DIR" + e2e_ready=0 + elif ! chmod 0644 "$TQFTP_TEST_SOURCE"; then + test_result_record "FAIL" "TQFTP E2E could not make its temporary source payload readable by the server" + e2e_ready=0 + fi + fi + fi + e2e_name=${TQFTP_TEST_SOURCE##*/} + if [ "$e2e_ready" -eq 1 ] && ! awk 'BEGIN { + for (line = 0; line < 64; line++) { + printf "QLI_TQFTP_E2E_%04d_0123456789abcdef\n", line + } + }' >"$TQFTP_TEST_SOURCE"; then + test_result_record "FAIL" "TQFTP E2E could not stage its temporary source payload at $TQFTP_TEST_SOURCE" + e2e_ready=0 + fi + if [ "$e2e_ready" -eq 1 ]; then + e2e_source_bytes=$(wc -c <"$TQFTP_TEST_SOURCE" | tr -d '[:space:]') + e2e_outer_timeout=$((TQFTP_TIMEOUT + 5)) + log_info "[TQFTP-E2E] phase=start client=python-AF_QIPCRTR server_node=$e2e_node service_port=$e2e_port remote_path=/readwrite/$e2e_name source_bytes=$e2e_source_bytes protocol_timeout=${TQFTP_TIMEOUT}s watchdog=${e2e_outer_timeout}s network=not-required" + run_with_timeout_log \ + "$e2e_outer_timeout" \ + "$e2e_log" \ + python3 "$TOOLS/tqftp_client.py" \ + --node "$e2e_node" \ + --port "$e2e_port" \ + --remote-path "/readwrite/$e2e_name" \ + --expected-file "$TQFTP_TEST_SOURCE" \ + --output-file "$e2e_received" \ + --timeout "$TQFTP_TIMEOUT" + e2e_rc=$? + log_file_with_label "TQFTP-E2E" "$e2e_log" 25 + if [ "$e2e_rc" -eq 0 ]; then + test_result_record "PASS" "TQFTP completed an exact local-host RRQ transfer over QRTR, bytes=$e2e_source_bytes artifact=$e2e_received" + elif [ "$e2e_rc" -eq 2 ]; then + test_result_record "SKIP" "The TQFTP E2E client reported that no functional registry input was selectable" + else + test_result_record "FAIL" "TQFTP local-host RRQ transfer failed, rc=$e2e_rc artifact=$e2e_log" + fi + fi + if [ -f "$TQFTP_TEST_SOURCE" ]; then + if rm -f "$TQFTP_TEST_SOURCE"; then + log_info "[TQFTP-E2E] phase=cleanup action=removed-temporary-source path=$TQFTP_TEST_SOURCE" + TQFTP_TEST_SOURCE="" + else + test_result_record "FAIL" "TQFTP E2E could not remove its temporary source file at $TQFTP_TEST_SOURCE" + fi + else + TQFTP_TEST_SOURCE="" + fi + fi +elif [ "$TQFTP_E2E_ENABLE" = "0" ]; then + test_result_record "SKIP" "TQFTP E2E transfer is disabled by policy, set --e2e 1 to enable it" +fi + +qrtr_capture_service_evidence \ + tqftpserv.service \ + tqftpserv \ + "$RESULT_DIR/service" \ + "$TQFTP_TIMEOUT" + +if grep -Ei '\[TQFTP\].*(WRQ|RRQ)|Remote returned END OF TRANSFER|opened for (reading|writing)' \ + "$RESULT_DIR/service/journal.log" >"$REQUEST_REPORT" 2>/dev/null; then + request_count=$(wc -l <"$REQUEST_REPORT" | tr -d '[:space:]') + log_info "[TQFTP-REQUEST] observed=$request_count artifact=$REQUEST_REPORT" + log_file_with_label "TQFTP-REQUEST-MARKER" "$REQUEST_REPORT" 20 + test_result_record "PASS" "Observed $request_count historical TQFTP request or transfer marker(s)" +else + : >"$REQUEST_REPORT" + journal_lines=$(wc -l <"$RESULT_DIR/service/journal.log" 2>/dev/null | tr -d '[:space:]') + log_info "[TQFTP-REQUEST] observed=0 action=none reason=no-remote-request-in-retained-journal journal_rc=${QCSE_JOURNAL_RC:-unknown} journal_lines=${journal_lines:-0} journal=$RESULT_DIR/service/journal.log readiness=passed" + test_result_record "SKIP" "No TQFTP firmware request was observed in the retained journal window" +fi + +export KERNEL_LOG_JOURNAL_FALLBACK=1 +scan_dmesg_errors \ + "$RESULT_DIR/kernel" \ + 'tqftp|qrtr|qcom_glink|glink|rpmsg|remoteproc' \ + 'endpoint is not connected' +dmesg_rc=$? +if [ "${DMESG_ACCESS_STATUS:-unavailable}" != "available" ]; then + test_result_record "SKIP" "Kernel log access is unavailable for TQFTP health validation, status=${DMESG_ACCESS_STATUS:-unknown} provider=${DMESG_ACCESS_PROVIDER:-none} rc=${DMESG_ACCESS_RC:-unknown} artifact=$RESULT_DIR/kernel/dmesg_access.log" +elif [ "$dmesg_rc" -eq 0 ]; then + log_file_with_label "TQFTP-KERNEL-ERROR" "$RESULT_DIR/kernel/dmesg_errors.log" 25 + test_result_record "FAIL" "TQFTP, QRTR, or remoteproc kernel errors were detected, artifact=$RESULT_DIR/kernel/dmesg_errors.log" +else + test_result_record "PASS" "No persistent TQFTP, QRTR, or remoteproc kernel errors were found" +fi + +if [ "$TEST_RESULT_FAIL_COUNT" -eq 0 ] && [ "$TQFTP_CORE_UNVERIFIED" -eq 1 ]; then + test_result_finish "SKIP" "$TESTNAME SKIP: TQFTP is active but its required QRTR service advertisement could not be verified" +fi + +test_result_finish diff --git a/Runner/utils/tqftp_client.py b/Runner/utils/tqftp_client.py new file mode 100755 index 00000000..ac0303bb --- /dev/null +++ b/Runner/utils/tqftp_client.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +"""Run one bounded read-only TQFTP transfer over AF_QIPCRTR.""" + +import argparse +import hashlib +import socket +import struct +import sys +from pathlib import Path + + +DEFAULT_BLOCK_SIZE = 512 +MAX_PACKET_LOGS = 16 + + +def fail(reason: str, **fields: object) -> int: + """Print one machine-readable failure record and return failure status.""" + details = " ".join(f"{key}={value}" for key, value in fields.items()) + print(f"TQFTP_E2E status=FAIL reason={reason} {details}".rstrip()) + return 1 + + +def parse_oack(payload: bytes): + """Parse and validate the numeric option pairs in a TFTP OACK payload.""" + fields = payload.split(b"\0") + if fields and fields[-1] == b"": + fields.pop() + if len(fields) % 2: + raise ValueError("odd option field count") + + options = {} + for index in range(0, len(fields), 2): + key = fields[index].decode("ascii").lower() + value = fields[index + 1].decode("ascii") + if not key or not value or not value.isdigit(): + raise ValueError("invalid option pair") + if key in options: + raise ValueError("duplicate option") + options[key] = int(value) + return options + + +def main() -> int: + """Run one bounded RRQ transfer and verify the received file exactly.""" + parser = argparse.ArgumentParser() + parser.add_argument("--node", type=int, required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--remote-path", required=True) + parser.add_argument("--expected-file", type=Path, required=True) + parser.add_argument("--output-file", type=Path, required=True) + parser.add_argument("--timeout", type=float, required=True) + args = parser.parse_args() + + family = getattr(socket, "AF_QIPCRTR", 42) + + try: + expected = args.expected_file.read_bytes() + except OSError as error: + return fail("expected-file-read", errno=error.errno, message=str(error)) + expected_size = len(expected) + request = ( + struct.pack("!H", 1) + + args.remote_path.encode() + + b"\0octet\0" + + b"blksize\0" + + str(DEFAULT_BLOCK_SIZE).encode() + + b"\0wsize\0" + + b"1\0rsize\0" + + str(expected_size).encode() + + b"\0" + ) + received = bytearray() + expected_block = 1 + packet_count = 0 + data_packet_count = 0 + packet_logs = 0 + oack_received = False + negotiated_block_size = DEFAULT_BLOCK_SIZE + negotiated_read_size = 0 + negotiated_window_size = 0 + transfer_port = "unknown" + transfer_source = None + + try: + with socket.socket(family, socket.SOCK_DGRAM) as client: + client.settimeout(args.timeout) + client.sendto(request, (args.node, args.port)) + while True: + packet, source = client.recvfrom(65536) + packet_count += 1 + if len(packet) < 2: + return fail("short-packet", packet_bytes=len(packet)) + opcode = struct.unpack("!H", packet[:2])[0] + transfer_port = source[1] + if transfer_source is None: + transfer_source = source + elif source != transfer_source: + return fail( + "transfer-source-changed", + expected_source=transfer_source, + observed_source=source, + ) + if opcode == 5: + if len(packet) < 4: + return fail("short-error-packet", packet_bytes=len(packet)) + block = struct.unpack("!H", packet[2:4])[0] + message = packet[4:].split(b"\0", 1)[0].decode(errors="replace") + return fail("server-error", code=block, message=message) + if opcode == 6: + if oack_received or data_packet_count: + return fail("unexpected-oack", packets=packet_count) + try: + options = parse_oack(packet[2:]) + except (UnicodeDecodeError, ValueError) as error: + return fail("invalid-oack", message=str(error)) + negotiated_block_size = options.get("blksize", 0) + negotiated_window_size = options.get("wsize", 0) + negotiated_read_size = options.get("rsize", 0) + if negotiated_block_size != DEFAULT_BLOCK_SIZE: + return fail( + "unexpected-oack-blksize", + expected=DEFAULT_BLOCK_SIZE, + observed=negotiated_block_size, + ) + if negotiated_window_size != 1: + return fail( + "unexpected-oack-wsize", + expected=1, + observed=negotiated_window_size, + ) + if negotiated_read_size != expected_size: + return fail( + "unexpected-oack-rsize", + expected=expected_size, + observed=negotiated_read_size, + ) + oack_received = True + print( + "TQFTP_PACKET phase=oack" + f" source_node={source[0]} source_port={source[1]}" + f" blksize={negotiated_block_size}" + f" wsize={negotiated_window_size}" + f" rsize={negotiated_read_size}" + ) + client.sendto(struct.pack("!HH", 4, 0), source) + continue + if opcode != 3: + return fail("unexpected-opcode", opcode=opcode) + if not oack_received: + return fail("data-before-oack", packets=packet_count) + if len(packet) < 4: + return fail("short-data-packet", packet_bytes=len(packet)) + block = struct.unpack("!H", packet[2:4])[0] + if block != expected_block: + return fail( + "unexpected-block", + expected_block=expected_block, + observed_block=block, + ) + payload = packet[4:] + received.extend(payload) + data_packet_count += 1 + if packet_logs < MAX_PACKET_LOGS: + print( + "TQFTP_PACKET phase=data" + f" block={block} payload_bytes={len(payload)}" + f" cumulative_bytes={len(received)}" + f" source_node={source[0]} source_port={source[1]}" + ) + packet_logs += 1 + client.sendto(struct.pack("!HH", 4, block), source) + expected_block = (expected_block + 1) & 0xFFFF + if len(received) >= expected_size: + break + except TimeoutError: + return fail( + "transfer-timeout", + packets=packet_count, + data_packets=data_packet_count, + expected_block=expected_block, + received_bytes=len(received), + expected_bytes=expected_size, + oack=int(oack_received), + transfer_port=transfer_port, + ) + except OSError as error: + return fail("socket-error", errno=error.errno, message=str(error)) + + try: + args.output_file.write_bytes(received) + except OSError as error: + return fail("output-file-write", errno=error.errno, message=str(error)) + expected_digest = hashlib.sha256(expected).hexdigest() + received_digest = hashlib.sha256(received).hexdigest() + if received != expected: + return fail( + "payload-mismatch", + expected_bytes=len(expected), + received_bytes=len(received), + expected_sha256=expected_digest, + received_sha256=received_digest, + ) + + if data_packet_count > packet_logs: + print( + "TQFTP_PACKET phase=data-summary" + f" omitted={data_packet_count - packet_logs}" + f" total_data_packets={data_packet_count}" + ) + + print( + "TQFTP_E2E status=PASS" + f" server_node={args.node} service_port={args.port}" + f" transfer_port={transfer_port} request_bytes={len(request)}" + f" packets={packet_count} data_packets={data_packet_count}" + f" blksize={negotiated_block_size} wsize={negotiated_window_size}" + f" rsize={negotiated_read_size} received_bytes={len(received)}" + f" sha256={received_digest} payload=verified" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 8f152ea35bf10daa3a33019582c1c512fcb4ce79 Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Fri, 18 Sep 2026 14:29:19 +0530 Subject: [PATCH 3/4] thermal: add controlled runtime validation Discover thermal zones, trip points, cooling bindings, and telemetry from the running target. Add an image-provided stress-ng phase with bounded sampling, recovery evidence, and kernel-log diagnostics, and enable that functional phase by default in the focused LAVA definition. Signed-off-by: Srikanth Muppandam --- .../Baseport/Thermal_Validation/README.md | 73 ++++ .../Thermal_Validation.yaml | 28 ++ .../Kernel/Baseport/Thermal_Validation/run.sh | 284 ++++++++++++++ Runner/utils/lib_thermal.sh | 358 ++++++++++++++++++ 4 files changed, 743 insertions(+) create mode 100644 Runner/suites/Kernel/Baseport/Thermal_Validation/README.md create mode 100755 Runner/suites/Kernel/Baseport/Thermal_Validation/Thermal_Validation.yaml create mode 100755 Runner/suites/Kernel/Baseport/Thermal_Validation/run.sh create mode 100755 Runner/utils/lib_thermal.sh diff --git a/Runner/suites/Kernel/Baseport/Thermal_Validation/README.md b/Runner/suites/Kernel/Baseport/Thermal_Validation/README.md new file mode 100644 index 00000000..02462f57 --- /dev/null +++ b/Runner/suites/Kernel/Baseport/Thermal_Validation/README.md @@ -0,0 +1,73 @@ +# Thermal validation + +`Thermal_Validation` exposes the existing device-tree thermal readiness check +as a focused suite and adds runtime trip-point and cooling-binding evidence. +The default path is read-only and portable across supported distributions. +Optional thermal ABI attributes remain informational when the kernel does not +expose them, and a cooling binding value of `-1` is accepted as unassociated. + +Thermal zones, trip points, bindings, sensors, and cooling devices are all +discovered from runtime DT and sysfs. Users do not supply a SoC-specific zone or +sensor list. This keeps the readiness path portable when different targets +expose different thermal policies. + +## Optional controlled load + +The load phase is disabled by default for direct `run.sh` execution and uses +only an image-provided `stress-ng`. The LAVA YAML enables it by default through +`THERMAL_LOAD_ENABLE=1`, because selecting this focused functional suite is the +CI opt-in to the bounded load. It checks that telemetry remains readable once +per second, records the maximum observed temperature rise and cooling-device +state increase, and samples again after a recovery interval. A small +temperature rise or no cooling-state increase is reported as a subcheck +`SKIP`, because workload placement, cooling, trip thresholds, and sensor +cadence vary by target. Command failure or lost final telemetry is a failure. + +`--load-enable 1` is an operator or CI policy, not an automatically inferred +capability. Enable it only where a bounded CPU load is acceptable. The duration, +worker count, recovery delay, and minimum-rise threshold tune that optional +phase; they do not describe the SoC thermal topology. + +There is intentionally no shell busy-loop fallback. `stress-ng` supplies a +bounded, auditable worker lifecycle and exit status, while an ad hoc loop can +distort scheduling, evade reliable cleanup, or provide misleading load proof. +Images without `stress-ng` retain full read-only thermal readiness coverage and +skip only the optional controlled-load subcheck. + +```sh +./run.sh +./run.sh --load-enable 1 --load-seconds 20 --load-workers 2 +``` + +| Option | Environment | `run.sh` default | YAML default | +|---|---|---:|---:| +| `--load-enable` | `THERMAL_LOAD_ENABLE` | `0` | `1` | +| `--load-seconds` | `THERMAL_LOAD_SECONDS` | `15` | `15` | +| `--load-workers` | `THERMAL_LOAD_WORKERS` | `1` | `1` | +| `--recovery-seconds` | `THERMAL_RECOVERY_SECONDS` | `5` | `5` | +| `--min-rise-mc` | `THERMAL_MIN_RISE_MC` | `1000` | `1000` | + +## Logs and results + +Look for `[THERMAL-SELECTION]`, `[THERMAL]`, `[THERMAL-POLICY]`, +`[THERMAL-TRIP]`, `[THERMAL-BINDING]`, `[THERMAL-LOAD]`, and +`[THERMAL-SAMPLE]`. Trip and binding details are printed up to 64 rows, followed +by an omitted count and artifact path for larger policies. Load samples are +summarized by phase with sensor counts, minimum and maximum temperatures, +hottest zone, and maximum cooling state rather than dumping every sample. +The generated `thermal_samples.tsv.summary.tsv` is retained, and summary parser +failure is reported as a test failure instead of being hidden by a shell +pipeline. +`[THERMAL-SELECTION]` records dynamic capability discovery and +whether the optional load policy was enabled. The printed +`results/Thermal_Validation/run-*/` directory retains zone, cooling, policy, +load, sample, and kernel evidence. + +- `PASS`: declared thermal runtime is valid and enabled phases complete. +- `FAIL`: declared zones or cooling devices are missing/malformed, load + execution fails, telemetry disappears, or relevant kernel errors are found. +- `SKIP`: thermal capability is absent or an optional load/tool/log is absent. + +Kernel health capture prefers `dmesg` and falls back to image-provided +`journalctl -k`. `kernel/dmesg_access.log` retains the provider and exact access +diagnostics when neither source is readable. diff --git a/Runner/suites/Kernel/Baseport/Thermal_Validation/Thermal_Validation.yaml b/Runner/suites/Kernel/Baseport/Thermal_Validation/Thermal_Validation.yaml new file mode 100755 index 00000000..04d974ef --- /dev/null +++ b/Runner/suites/Kernel/Baseport/Thermal_Validation/Thermal_Validation.yaml @@ -0,0 +1,28 @@ +metadata: + name: Thermal_Validation + format: "Lava-Test Test Definition 1.0" + description: "Dynamically validate thermal policy and optionally exercise controlled-load response" + os: + - linux + scope: + - functional + +params: + THERMAL_LOAD_ENABLE: "1" + THERMAL_LOAD_SECONDS: "15" + THERMAL_LOAD_WORKERS: "1" + THERMAL_RECOVERY_SECONDS: "5" + THERMAL_MIN_RISE_MC: "1000" + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Kernel/Baseport/Thermal_Validation + - >- + ./run.sh + --load-enable "${THERMAL_LOAD_ENABLE}" + --load-seconds "${THERMAL_LOAD_SECONDS}" + --load-workers "${THERMAL_LOAD_WORKERS}" + --recovery-seconds "${THERMAL_RECOVERY_SECONDS}" + --min-rise-mc "${THERMAL_MIN_RISE_MC}" || true + - $REPO_PATH/Runner/utils/send-to-lava.sh Thermal_Validation.res diff --git a/Runner/suites/Kernel/Baseport/Thermal_Validation/run.sh b/Runner/suites/Kernel/Baseport/Thermal_Validation/run.sh new file mode 100755 index 00000000..1667a70f --- /dev/null +++ b/Runner/suites/Kernel/Baseport/Thermal_Validation/run.sh @@ -0,0 +1,284 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# ---------- Repo env + helpers ---------- +SCRIPT_DIR="$( + cd "$(dirname "$0")" || exit 1 + pwd +)" +INIT_ENV="" +SEARCH="$SCRIPT_DIR" + +while [ "$SEARCH" != "/" ]; do + if [ -f "$SEARCH/init_env" ]; then + INIT_ENV="$SEARCH/init_env" + break + fi + SEARCH=$(dirname "$SEARCH") +done + +if [ -z "$INIT_ENV" ]; then + echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 + exit 1 +fi + +# Only source once (idempotent) +# NOTE: We intentionally **do not export** any new vars. They stay local to this shell. +if [ -z "${__INIT_ENV_LOADED:-}" ]; then + # shellcheck disable=SC1090 + . "$INIT_ENV" + __INIT_ENV_LOADED=1 +fi + +# shellcheck disable=SC1090 +. "$INIT_ENV" +# shellcheck disable=SC1091 +. "$TOOLS/functestlib.sh" +# shellcheck disable=SC1091 +. "$TOOLS/lib_thermal.sh" +# shellcheck disable=SC1091 +. "$TOOLS/lib_system.sh" +TESTNAME="Thermal_Validation" +RES_FILE="$SCRIPT_DIR/$TESTNAME.res" + +THERMAL_LOAD_ENABLE="${THERMAL_LOAD_ENABLE:-0}" +THERMAL_LOAD_SECONDS="${THERMAL_LOAD_SECONDS:-15}" +THERMAL_LOAD_WORKERS="${THERMAL_LOAD_WORKERS:-1}" +THERMAL_RECOVERY_SECONDS="${THERMAL_RECOVERY_SECONDS:-5}" +THERMAL_MIN_RISE_MC="${THERMAL_MIN_RISE_MC:-1000}" +RESULT_DIR="$SCRIPT_DIR/results/$TESTNAME/run-$(date '+%Y%m%d-%H%M%S')-$$" +DT_ROOT="" + +# cleanup +# Takes no arguments and produces no stdout. Stops only the controlled-load +# process started by this suite, preserves retained evidence, and restores the +# runner stdout capture when the script exits or receives a handled signal. +cleanup() { + cleanup_status=$? + thermal_stop_controlled_load >/dev/null 2>&1 || true + [ "$cleanup_status" -eq 0 ] + _runner_stdout_cleanup +} + +# usage +# Takes no arguments, prints the supported CLI contract to stdout, and has no +# side effects. +usage() { + printf '%s\n' \ + "Usage: ./run.sh [options]" \ + " --load-enable 0|1 Optional stress phase, default: 0" \ + " --load-seconds SECONDS Stress duration, default: 15" \ + " --load-workers COUNT CPU workers, default: 1" \ + " --recovery-seconds SECONDS Recovery sample delay, default: 5" \ + " --min-rise-mc MILLICELSIUS Informational response threshold, default: 1000" \ + " -h, --help" \ + "Thermal zones, trips, and cooling devices are discovered dynamically." \ + "CLI options override environment variables." +} + +# parse_args +# Applies option values to the suite configuration globals. Produces no stdout, +# returns 0 on success or 2 for a missing or unknown argument, and does not +# start validation or mutate target state. +parse_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + --load-enable) + [ "$#" -ge 2 ] || return 2 + THERMAL_LOAD_ENABLE="$2" + shift 2 + ;; + --load-seconds) + [ "$#" -ge 2 ] || return 2 + THERMAL_LOAD_SECONDS="$2" + shift 2 + ;; + --load-workers) + [ "$#" -ge 2 ] || return 2 + THERMAL_LOAD_WORKERS="$2" + shift 2 + ;; + --recovery-seconds) + [ "$#" -ge 2 ] || return 2 + THERMAL_RECOVERY_SECONDS="$2" + shift 2 + ;; + --min-rise-mc) + [ "$#" -ge 2 ] || return 2 + THERMAL_MIN_RISE_MC="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + log_error "Unknown argument: $1" + return 2 + ;; + esac + done +} + +parse_args "$@" || { + usage >&2 + exit 2 +} + +test_result_init "$TESTNAME" "$RES_FILE" +trap cleanup EXIT HUP INT TERM +if ! mkdir -p "$RESULT_DIR"; then + test_result_finish "FAIL" "$TESTNAME FAIL: cannot create retained evidence directory $RESULT_DIR" +fi + +log_info "--------------------------------------------------------------------------" +log_info "Starting $TESTNAME" +log_info "Evidence directory: $RESULT_DIR" +log_info "Thermal validation: checking DT declarations, runtime zones, trips, cooling bindings, and optional controlled-load response" +log_info "Configuration: load_enable=$THERMAL_LOAD_ENABLE load_seconds=${THERMAL_LOAD_SECONDS}s workers=$THERMAL_LOAD_WORKERS recovery_seconds=${THERMAL_RECOVERY_SECONDS}s min_rise_mC=$THERMAL_MIN_RISE_MC" +log_info "[THERMAL-SELECTION] capability=dynamic load_policy=$THERMAL_LOAD_ENABLE" + +for thermal_value in \ + "$THERMAL_LOAD_SECONDS" \ + "$THERMAL_LOAD_WORKERS" \ + "$THERMAL_RECOVERY_SECONDS" \ + "$THERMAL_MIN_RISE_MC"; do + if ! system_is_uint "$thermal_value"; then + test_result_record "FAIL" "Thermal load settings must be unsigned integers" + test_result_finish + fi +done + +if [ "$THERMAL_LOAD_SECONDS" -eq 0 ] || [ "$THERMAL_LOAD_WORKERS" -eq 0 ]; then + test_result_record "FAIL" "Thermal load duration and worker count must be positive integers" + test_result_finish +fi + +case "$THERMAL_LOAD_ENABLE" in + 0|1) + ;; + *) + test_result_record "FAIL" "Thermal load enable must be 0 or 1" + test_result_finish + ;; +esac + +for thermal_dt_candidate in /proc/device-tree /sys/firmware/devicetree/base; do + [ -d "$thermal_dt_candidate" ] || continue + DT_ROOT=$(readlink -f "$thermal_dt_candidate") + break +done + +if [ -z "$DT_ROOT" ]; then + test_result_record "SKIP" "Runtime device tree is unavailable for thermal applicability" + test_result_finish +fi + +thermal_declared_count=0 +if [ -d "$DT_ROOT/thermal-zones" ]; then + for thermal_declared_zone in "$DT_ROOT/thermal-zones"/*; do + [ -d "$thermal_declared_zone" ] || continue + dt_node_enabled "$thermal_declared_zone" || continue + thermal_declared_count=$((thermal_declared_count + 1)) + done +fi + +if [ "$thermal_declared_count" -eq 0 ]; then + log_info "[THERMAL-DISCOVERY] dt_root=$DT_ROOT enabled_zones=0 reason=no-enabled-thermal-zones" + test_result_record "SKIP" "Runtime device tree has no enabled thermal zones" + test_result_finish +fi + +log_info "[THERMAL-DISCOVERY] dt_root=$DT_ROOT enabled_zones=$thermal_declared_count" + +dt_validate_thermal_runtime "$DT_ROOT" "$RESULT_DIR" + +if thermal_capture_policy "$RESULT_DIR/thermal_policy.tsv"; then + log_info "[THERMAL-POLICY] trips=$THERMAL_TRIP_COUNT bindings=$THERMAL_BINDING_COUNT invalid=0 artifact=$RESULT_DIR/thermal_policy.tsv" + thermal_log_policy "$RESULT_DIR/thermal_policy.tsv" 64 + if [ "$THERMAL_TRIP_COUNT" -gt 0 ]; then + test_result_record "PASS" "Thermal runtime exposes $THERMAL_TRIP_COUNT valid trip point(s) and $THERMAL_BINDING_COUNT cooling binding(s)" + else + test_result_record "SKIP" "No runtime trip-point attributes are exposed" + fi +else + log_fail "[THERMAL-POLICY] trips=$THERMAL_TRIP_COUNT bindings=$THERMAL_BINDING_COUNT invalid=$THERMAL_POLICY_INVALID_COUNT artifact=$RESULT_DIR/thermal_policy.tsv" + thermal_log_policy "$RESULT_DIR/thermal_policy.tsv" 64 + test_result_record "FAIL" "Thermal policy exposes $THERMAL_POLICY_INVALID_COUNT malformed trip attribute(s)" +fi + +if [ "$THERMAL_LOAD_ENABLE" = "1" ]; then + if ! command -v stress-ng >/dev/null 2>&1; then + test_result_record "SKIP" "Controlled thermal load was requested but stress-ng is not image-provided" + else + : >"$RESULT_DIR/thermal_samples.tsv" + if ! thermal_capture_sample "$RESULT_DIR/thermal_samples.tsv" before; then + test_result_record "FAIL" "No readable temperature was available before controlled load" + else + load_timeout=$((THERMAL_LOAD_SECONDS + 10)) + log_info "[THERMAL-LOAD] action=start tool=stress-ng workers=$THERMAL_LOAD_WORKERS duration=${THERMAL_LOAD_SECONDS}s timeout=${load_timeout}s sample_interval=1s" + thermal_run_controlled_load \ + "$RESULT_DIR/thermal_samples.tsv" \ + "$RESULT_DIR/stress-ng.log" \ + "$THERMAL_LOAD_SECONDS" \ + "$THERMAL_LOAD_WORKERS" + load_rc=$? + thermal_capture_sample "$RESULT_DIR/thermal_samples.tsv" after + after_sample_rc=$? + + if [ "$load_rc" -ne 0 ]; then + log_file_with_label "THERMAL-STRESS" "$RESULT_DIR/stress-ng.log" 25 + test_result_record "FAIL" "Controlled thermal load failed, rc=$load_rc artifact=$RESULT_DIR/stress-ng.log" + elif [ "$after_sample_rc" -ne 0 ]; then + test_result_record "FAIL" "No readable temperature was available after controlled load" + else + max_rise=$(thermal_sample_max_rise "$RESULT_DIR/thermal_samples.tsv" 2>/dev/null || true) + max_cooling_increase=$(thermal_sample_max_cooling_increase "$RESULT_DIR/thermal_samples.tsv" 2>/dev/null || true) + log_info "[THERMAL-LOAD] action=complete rc=0 sample_failures=$THERMAL_LOAD_SAMPLE_FAILURES max_rise_mC=${max_rise:-unknown} max_cooling_increase=${max_cooling_increase:-unavailable} samples=$RESULT_DIR/thermal_samples.tsv" + test_result_record "PASS" "Controlled CPU load completed and thermal telemetry remained readable" + if [ -n "$max_rise" ] && [ "$max_rise" -ge "$THERMAL_MIN_RISE_MC" ]; then + test_result_record "PASS" "Thermal response was observed, maximum temperature rise=${max_rise}mC" + else + test_result_record "SKIP" "No temperature rise of at least ${THERMAL_MIN_RISE_MC}mC was observed during the bounded load" + fi + if [ -n "$max_cooling_increase" ] && [ "$max_cooling_increase" -gt 0 ]; then + test_result_record "PASS" "A cooling-device state increase was observed during controlled load" + else + test_result_record "SKIP" "No cooling-device state increase was required during the bounded load" + fi + fi + + if [ "$THERMAL_RECOVERY_SECONDS" -gt 0 ]; then + sleep "$THERMAL_RECOVERY_SECONDS" + if thermal_capture_sample "$RESULT_DIR/thermal_samples.tsv" recovery; then + test_result_record "PASS" "Thermal telemetry remained readable after ${THERMAL_RECOVERY_SECONDS}s recovery" + else + test_result_record "FAIL" "Thermal telemetry became unreadable during recovery" + fi + fi + if ! thermal_log_sample_summary "$RESULT_DIR/thermal_samples.tsv"; then + test_result_record "FAIL" "Thermal sample summary generation failed, samples=$RESULT_DIR/thermal_samples.tsv" + fi + fi + fi +else + test_result_record "SKIP" "Controlled thermal load is disabled by default, set --load-enable 1 to opt in" +fi + +export KERNEL_LOG_JOURNAL_FALLBACK=1 +scan_dmesg_errors \ + "$RESULT_DIR/kernel" \ + 'thermal|tsens|lmh|cooling|cpufreq' \ + 'critical temperature reached' +dmesg_rc=$? +if [ "${DMESG_ACCESS_STATUS:-unavailable}" != "available" ]; then + test_result_record "SKIP" "Kernel log access is unavailable for thermal health validation, status=${DMESG_ACCESS_STATUS:-unknown} provider=${DMESG_ACCESS_PROVIDER:-none} rc=${DMESG_ACCESS_RC:-unknown} artifact=$RESULT_DIR/kernel/dmesg_access.log" +elif [ "$dmesg_rc" -eq 0 ]; then + log_file_with_label "THERMAL-KERNEL-ERROR" "$RESULT_DIR/kernel/dmesg_errors.log" 25 + test_result_record "FAIL" "Thermal-related kernel errors were detected, artifact=$RESULT_DIR/kernel/dmesg_errors.log" +else + test_result_record "PASS" "No persistent thermal-related kernel errors were found" +fi + +test_result_finish diff --git a/Runner/utils/lib_thermal.sh b/Runner/utils/lib_thermal.sh new file mode 100755 index 00000000..a59f44b0 --- /dev/null +++ b/Runner/utils/lib_thermal.sh @@ -0,0 +1,358 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# thermal_capture_policy +# Captures runtime trip points and cooling-device bindings for all zones into a +# tab-separated artifact. Produces no stdout, exports policy counters, and +# returns 0 only when every discovered value is valid, 1 on capture or policy +# failure, or 3 for a missing output path. It does not change thermal state. +thermal_capture_policy() { + tcp_output_file="$1" + + THERMAL_TRIP_COUNT=0 + THERMAL_BINDING_COUNT=0 + THERMAL_POLICY_INVALID_COUNT=0 + + [ -n "$tcp_output_file" ] || return 3 + : >"$tcp_output_file" || return 1 + + for tcp_zone in /sys/class/thermal/thermal_zone*; do + [ -d "$tcp_zone" ] || continue + tcp_zone_name=${tcp_zone##*/} + tcp_zone_type=$(cat "$tcp_zone/type" 2>/dev/null || true) + + for tcp_trip_temp_file in "$tcp_zone"/trip_point_*_temp; do + [ -r "$tcp_trip_temp_file" ] || continue + tcp_trip_base=${tcp_trip_temp_file%_temp} + tcp_trip_name=${tcp_trip_base##*/} + tcp_trip_index=${tcp_trip_name#trip_point_} + tcp_trip_temp=$(cat "$tcp_trip_temp_file" 2>/dev/null || true) + tcp_trip_type=$(cat "${tcp_trip_base}_type" 2>/dev/null || true) + THERMAL_TRIP_COUNT=$((THERMAL_TRIP_COUNT + 1)) + + case "$tcp_trip_temp" in + -* ) + tcp_trip_digits=${tcp_trip_temp#-} + ;; + *) + tcp_trip_digits=$tcp_trip_temp + ;; + esac + case "$tcp_trip_digits" in + ''|*[!0-9]*) + THERMAL_POLICY_INVALID_COUNT=$((THERMAL_POLICY_INVALID_COUNT + 1)) + ;; + *) + if [ "$tcp_trip_temp" -lt -100000 ] || + [ "$tcp_trip_temp" -gt 250000 ]; then + THERMAL_POLICY_INVALID_COUNT=$((THERMAL_POLICY_INVALID_COUNT + 1)) + fi + ;; + esac + printf 'trip\t%s\t%s\t%s\t%s\n' \ + "$tcp_zone_name" \ + "${tcp_zone_type:-unknown}" \ + "$tcp_trip_index" \ + "${tcp_trip_type:-unknown}:temp_mC=${tcp_trip_temp:-unreadable}" >>"$tcp_output_file" + done + + for tcp_cdev_link in "$tcp_zone"/cdev[0-9]*; do + [ -L "$tcp_cdev_link" ] || continue + tcp_cdev=$(readlink -f "$tcp_cdev_link") + tcp_cdev_name=${tcp_cdev##*/} + tcp_link_name=${tcp_cdev_link##*/} + tcp_trip_binding=$(cat "$tcp_zone/${tcp_link_name}_trip_point" 2>/dev/null || true) + tcp_weight=$(cat "$tcp_zone/${tcp_link_name}_weight" 2>/dev/null || true) + THERMAL_BINDING_COUNT=$((THERMAL_BINDING_COUNT + 1)) + case "$tcp_trip_binding" in + -1) + ;; + ''|*[!0-9]*) + THERMAL_POLICY_INVALID_COUNT=$((THERMAL_POLICY_INVALID_COUNT + 1)) + ;; + esac + case "$tcp_weight" in + '') + ;; + *[!0-9]*) + THERMAL_POLICY_INVALID_COUNT=$((THERMAL_POLICY_INVALID_COUNT + 1)) + ;; + esac + printf 'binding\t%s\t%s\ttrip=%s\tweight=%s\n' \ + "$tcp_zone_name" \ + "$tcp_cdev_name" \ + "${tcp_trip_binding:-unknown}" \ + "${tcp_weight:-unknown}" >>"$tcp_output_file" + done + done + + export THERMAL_TRIP_COUNT THERMAL_BINDING_COUNT THERMAL_POLICY_INVALID_COUNT + [ "$THERMAL_POLICY_INVALID_COUNT" -eq 0 ] +} + +# thermal_log_policy [max-rows] +# Logs bounded trip and cooling-binding details retained by +# thermal_capture_policy. Produces no machine-readable stdout, returns 0 on +# success, 1 for an unreadable artifact, or 3 for an invalid row limit, and +# does not change thermal state. +thermal_log_policy() { + tlp_policy_file="$1" + tlp_max_rows="${2:-64}" + tlp_total=0 + tlp_emitted=0 + + [ -r "$tlp_policy_file" ] || return 1 + case "$tlp_max_rows" in + ''|*[!0-9]*|0) + return 3 + ;; + esac + + tlp_total=$(wc -l <"$tlp_policy_file" 2>/dev/null | tr -d '[:space:]') + while IFS="$(printf '\t')" read -r tlp_kind tlp_zone tlp_object tlp_value tlp_extra; do + if [ "$tlp_emitted" -ge "$tlp_max_rows" ]; then + break + fi + case "$tlp_kind" in + trip) + log_info "[THERMAL-TRIP] zone=$tlp_zone type=$tlp_object index=$tlp_value detail=$tlp_extra" + ;; + binding) + log_info "[THERMAL-BINDING] zone=$tlp_zone cooling=$tlp_object trip=${tlp_value#trip=} weight=${tlp_extra#weight=}" + ;; + *) + log_warn "[THERMAL-POLICY-ROW] kind=${tlp_kind:-missing} zone=${tlp_zone:-missing} observed=malformed" + ;; + esac + tlp_emitted=$((tlp_emitted + 1)) + done <"$tlp_policy_file" + + if [ "$tlp_total" -gt "$tlp_emitted" ]; then + log_info "[THERMAL-POLICY-ROW] omitted=$((tlp_total - tlp_emitted)) total=$tlp_total artifact=$tlp_policy_file" + fi +} + +# thermal_log_sample_summary +# Builds a retained TSV summary and logs phase-level temperature ranges, +# hottest zones, and cooling-state maxima. Produces no machine-readable stdout, +# returns 0 on success or 1 for unreadable, malformed, or empty evidence, and +# does not change thermal state. +thermal_log_sample_summary() { + tlss_sample_file="$1" + tlss_summary_file="${tlss_sample_file}.summary.tsv" + + [ -r "$tlss_sample_file" ] || return 1 + if ! awk -F '\t' ' + { + phase=$2 + if (phase ~ /^load-/) { + phase="load" + } + } + $3 == "zone" { + zone_count[phase]++ + if (!(phase in min_temp) || $5 < min_temp[phase]) { + min_temp[phase]=$5 + } + if (!(phase in max_temp) || $5 > max_temp[phase]) { + max_temp[phase]=$5 + hottest[phase]=$4 + } + } + $3 == "cooling" { + cooling_count[phase]++ + if (!(phase in max_cooling) || $5 > max_cooling[phase]) { + max_cooling[phase]=$5 + } + } + END { + order[1]="before" + order[2]="load" + order[3]="after" + order[4]="recovery" + for (phase_index=1; phase_index<=4; phase_index++) { + phase=order[phase_index] + if (zone_count[phase] > 0 || cooling_count[phase] > 0) { + printf "%s\t%d\t%s\t%s\t%s\t%d\t%s\n", phase, + zone_count[phase] + 0, + (phase in min_temp ? min_temp[phase] : "unavailable"), + (phase in max_temp ? max_temp[phase] : "unavailable"), + (phase in hottest ? hottest[phase] : "unavailable"), + cooling_count[phase] + 0, + (phase in max_cooling ? max_cooling[phase] : "unavailable") + } + } + } + ' "$tlss_sample_file" >"$tlss_summary_file"; then + return 1 + fi + + [ -s "$tlss_summary_file" ] || return 1 + while IFS="$(printf '\t')" read -r tlss_phase tlss_zones tlss_min tlss_max tlss_hottest tlss_cooling tlss_cooling_max; do + log_info "[THERMAL-SAMPLE] phase=$tlss_phase zones=$tlss_zones min_temp_mC=$tlss_min max_temp_mC=$tlss_max hottest_zone=$tlss_hottest cooling_devices=$tlss_cooling max_cooling_state=$tlss_cooling_max samples=$tlss_sample_file summary=$tlss_summary_file" + done <"$tlss_summary_file" +} + +# thermal_capture_sample +# Appends one timestamped phase sample of readable thermal-zone temperatures +# and cooling-device states to the supplied TSV file. Produces no stdout and +# returns 0 when at least one temperature is readable, 1 otherwise, or 3 for +# invalid arguments. It does not change thermal state. +thermal_capture_sample() { + tcs_output_file="$1" + tcs_phase="$2" + tcs_timestamp=$(date +%s) + tcs_readable=0 + + [ -n "$tcs_output_file" ] && [ -n "$tcs_phase" ] || return 3 + + for tcs_zone in /sys/class/thermal/thermal_zone*; do + [ -d "$tcs_zone" ] || continue + tcs_temp=$(cat "$tcs_zone/temp" 2>/dev/null || true) + case "$tcs_temp" in + -*) + tcs_temp_digits=${tcs_temp#-} + ;; + *) + tcs_temp_digits=$tcs_temp + ;; + esac + case "$tcs_temp_digits" in + ''|*[!0-9]*) + continue + ;; + esac + tcs_readable=$((tcs_readable + 1)) + printf '%s\t%s\tzone\t%s\t%s\n' \ + "$tcs_timestamp" \ + "$tcs_phase" \ + "${tcs_zone##*/}" \ + "$tcs_temp" >>"$tcs_output_file" + done + + for tcs_cdev in /sys/class/thermal/cooling_device*; do + [ -d "$tcs_cdev" ] || continue + tcs_state=$(cat "$tcs_cdev/cur_state" 2>/dev/null || true) + case "$tcs_state" in + ''|*[!0-9]*) + continue + ;; + esac + printf '%s\t%s\tcooling\t%s\t%s\n' \ + "$tcs_timestamp" \ + "$tcs_phase" \ + "${tcs_cdev##*/}" \ + "$tcs_state" >>"$tcs_output_file" + done + + [ "$tcs_readable" -gt 0 ] +} + +# thermal_sample_max_rise +# Prints one integer in milli-Celsius representing the largest load or +# post-load increase over the pre-load temperature. Returns 0 with a value, 1 +# when no comparable samples exist, or 3 for an unreadable input artifact. +thermal_sample_max_rise() { + tsmr_sample_file="$1" + + [ -r "$tsmr_sample_file" ] || return 3 + awk -F '\t' ' + $3 == "zone" && $2 == "before" { before[$4]=$5 } + $3 == "zone" && $2 != "before" && $2 != "recovery" && ($4 in before) { + delta=$5-before[$4] + if (!seen || delta > maximum) { + maximum=delta + seen=1 + } + } + END { + if (!seen) { + exit 1 + } + print maximum + } + ' "$tsmr_sample_file" +} + +# thermal_sample_max_cooling_increase +# Prints the largest cooling-device state increase observed during or after +# the controlled load. Zero is a valid stdout result when no throttling was +# required. Returns 0 with a value, 1 when no comparable samples exist, or 3 +# for an unreadable input artifact. +thermal_sample_max_cooling_increase() { + tsmci_sample_file="$1" + + [ -r "$tsmci_sample_file" ] || return 3 + awk -F '\t' ' + $3 == "cooling" && $2 == "before" { before[$4]=$5 } + $3 == "cooling" && $2 != "before" && $2 != "recovery" && ($4 in before) { + delta=$5-before[$4] + if (!seen || delta > maximum) { + maximum=delta + seen=1 + } + } + END { + if (!seen) { + exit 1 + } + print maximum + } + ' "$tsmci_sample_file" +} + +# thermal_stop_controlled_load +# Takes no arguments and produces no stdout. Stops and reaps only the process +# identified by THERMAL_CONTROLLED_LOAD_PID, clears that exported ownership +# variable, and leaves retained load evidence in place. +thermal_stop_controlled_load() { + if [ -n "${THERMAL_CONTROLLED_LOAD_PID:-}" ] && + kill -0 "$THERMAL_CONTROLLED_LOAD_PID" 2>/dev/null; then + kill "$THERMAL_CONTROLLED_LOAD_PID" 2>/dev/null || true + wait "$THERMAL_CONTROLLED_LOAD_PID" 2>/dev/null || true + fi + THERMAL_CONTROLLED_LOAD_PID="" + export THERMAL_CONTROLLED_LOAD_PID +} + +# thermal_run_controlled_load +# Runs stress-ng with its own duration plus an outer watchdog while collecting +# one thermal and cooling-state sample per second. Produces no stdout, exports +# process ownership and sample-failure counters, returns the stress-ng status or +# 124 on watchdog expiry, and leaves cleanup to thermal_stop_controlled_load. +thermal_run_controlled_load() { + trcl_sample_file="$1" + trcl_log_file="$2" + trcl_seconds="$3" + trcl_workers="$4" + trcl_elapsed=0 + trcl_limit=$((trcl_seconds + 10)) + + THERMAL_LOAD_SAMPLE_FAILURES=0 + stress-ng --cpu "$trcl_workers" --timeout "${trcl_seconds}s" --metrics-brief \ + >"$trcl_log_file" 2>&1 & + THERMAL_CONTROLLED_LOAD_PID=$! + export THERMAL_CONTROLLED_LOAD_PID + + while kill -0 "$THERMAL_CONTROLLED_LOAD_PID" 2>/dev/null; do + if ! thermal_capture_sample \ + "$trcl_sample_file" \ + "load-$trcl_elapsed"; then + THERMAL_LOAD_SAMPLE_FAILURES=$((THERMAL_LOAD_SAMPLE_FAILURES + 1)) + fi + if [ "$trcl_elapsed" -ge "$trcl_limit" ]; then + thermal_stop_controlled_load + export THERMAL_LOAD_SAMPLE_FAILURES + return 124 + fi + sleep 1 + trcl_elapsed=$((trcl_elapsed + 1)) + done + + wait "$THERMAL_CONTROLLED_LOAD_PID" + trcl_rc=$? + THERMAL_CONTROLLED_LOAD_PID="" + export THERMAL_CONTROLLED_LOAD_PID THERMAL_LOAD_SAMPLE_FAILURES + return "$trcl_rc" +} From 71faa7fc57f8779551bb118835cd94d63a04b8fb Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Fri, 18 Sep 2026 14:31:48 +0530 Subject: [PATCH 4/4] pd-mapper: add kernel and functional validation Prefer the kernel PD Mapper implementation while retaining a userspace fallback. Discover the protocol endpoint and service-registry data dynamically, then issue a bounded QMI domain-list request and verify the returned domains against the runtime registry. Signed-off-by: Srikanth Muppandam --- .../PD_Mapper_Validation.yaml | 19 + .../Baseport/PD_Mapper_Validation/README.md | 105 +++++ .../Baseport/PD_Mapper_Validation/run.sh | 305 ++++++++++++++ Runner/utils/pd_mapper_client.py | 374 ++++++++++++++++++ 4 files changed, 803 insertions(+) create mode 100755 Runner/suites/Kernel/Baseport/PD_Mapper_Validation/PD_Mapper_Validation.yaml create mode 100644 Runner/suites/Kernel/Baseport/PD_Mapper_Validation/README.md create mode 100755 Runner/suites/Kernel/Baseport/PD_Mapper_Validation/run.sh create mode 100755 Runner/utils/pd_mapper_client.py diff --git a/Runner/suites/Kernel/Baseport/PD_Mapper_Validation/PD_Mapper_Validation.yaml b/Runner/suites/Kernel/Baseport/PD_Mapper_Validation/PD_Mapper_Validation.yaml new file mode 100755 index 00000000..635b4376 --- /dev/null +++ b/Runner/suites/Kernel/Baseport/PD_Mapper_Validation/PD_Mapper_Validation.yaml @@ -0,0 +1,19 @@ +metadata: + name: PD_Mapper_Validation + format: "Lava-Test Test Definition 1.0" + description: "Dynamically validate kernel or userspace PD Mapper readiness, registry data, QRTR advertisement, and a QMI domain lookup" + os: + - linux + scope: + - functional + +params: + PD_MAPPER_TIMEOUT: "10" + PD_MAPPER_SERVICE: "" + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Kernel/Baseport/PD_Mapper_Validation + - ./run.sh --timeout "${PD_MAPPER_TIMEOUT}" --service "${PD_MAPPER_SERVICE}" || true + - $REPO_PATH/Runner/utils/send-to-lava.sh PD_Mapper_Validation.res diff --git a/Runner/suites/Kernel/Baseport/PD_Mapper_Validation/README.md b/Runner/suites/Kernel/Baseport/PD_Mapper_Validation/README.md new file mode 100644 index 00000000..15c524c0 --- /dev/null +++ b/Runner/suites/Kernel/Baseport/PD_Mapper_Validation/README.md @@ -0,0 +1,105 @@ +# PD Mapper validation + +`PD_Mapper_Validation` checks the Qualcomm PD Mapper runtime without assuming a +board-specific process-domain list. It prefers the modern kernel implementation +exposed through `CONFIG_QCOM_PD_MAPPER`, the `qcom-pdm-mapper` auxiliary driver, +and runtime devices named `qcom_common.pd-mapper.*`. When those runtime devices +are absent, it falls back to an installed `pd-mapper.service` or running +`pd-mapper` process. A binary in `PATH` alone does not make the test applicable. + +This follows the public kernel implementation in +[`drivers/soc/qcom/qcom_pd_mapper.c`](https://github.com/qualcomm-linux/kernel/blob/qcom-next/drivers/soc/qcom/qcom_pd_mapper.c), +the `CONFIG_QCOM_PD_MAPPER` description in +[`drivers/soc/qcom/Kconfig`](https://github.com/qualcomm-linux/kernel/blob/qcom-next/drivers/soc/qcom/Kconfig), +and the auxiliary-device creation in +[`drivers/remoteproc/qcom_common.c`](https://github.com/qualcomm-linux/kernel/blob/qcom-next/drivers/remoteproc/qcom_common.c). +The fallback follows the public +[`linux-msm/pd-mapper`](https://github.com/linux-msm/pd-mapper) daemon and the +[`meta-qcom` recipe](https://github.com/qualcomm-linux/meta-qcom/blob/master/recipes-support/pd-mapper/pd-mapper_1.1.bb). + +The suite is not configured per SoC. Run `./run.sh` without target-specific +parameters. It dynamically checks whether PD Mapper applies, discovers the live +QRTR topology, and derives service-registry files from running remote processors. +The required `64:1:1` QRTR tuple is common to the public PD Mapper protocol +rather than a board-specific service guess. The kernel and userspace sources +declare QMI version `0x101`; QRTR packs that value into an eight-bit displayed +version and the upper instance bits, which `qrtr-lookup` renders as version `1`, +instance `1`. + +When Python and runtime registry data are available, the suite also sends the +read-only QMI `SERVREG_LOC_GET_DOMAIN_LIST` request (`0x21`) to the unique live +mapper endpoint. By default it tries a bounded, deterministic set of services +derived from the registry and selects the first service with returned domains. +It requires the returned domain and instance set to match that registry. If a +kernel mapper has no registry service in common, it queries the public kernel +contract `tms/servreg` and requires a nonempty, structurally valid domain list. +The same kernel query is used when no registry file is installed. +The userspace daemon has no such synthetic service, so its functional result +must match registry data. +It uses local AF_QIPCRTR communication and does not require Ethernet, Wi-Fi, or +access to an external server. The client uses Python's named address-family +constant when available and the public Linux family number otherwise. + +This discovery model is portable across image types. meta-qcom Yocto images can +provide either the kernel driver or the userspace daemon depending on the +machine and image revision. Ubuntu and Debian images are not assumed to install +either implementation, so the same runtime kernel, unit, process, QRTR, and +registry checks decide applicability without installing packages. + +Applicability comes from runtime auxiliary devices or the live protocol tuple, +not from kernel configuration alone. Every discovered auxiliary device must be +bound to the registered auxiliary driver whose kernel-generated name ends in +`.qcom-pdm-mapper`, and an instantiated mapper must advertise the public service +registry locator tuple `64:1:1`. The suite also derives service-registry `.jsn` and +`.jsn.xz` files from the firmware paths of running remote processors. When +Python is image-provided, those files are parsed and checked for the public +`sr_domain` object and `sr_service` array. Missing optional registry files or a +JSON validator is reported as a subcheck `SKIP`, not hidden as success. + +## Run + +```sh +./run.sh +./run.sh --timeout 15 +./run.sh --service avs/audio +``` + +`--timeout` overrides `PD_MAPPER_TIMEOUT`, whose default is 10 seconds. It only +changes probe bounds and does not select a target or service. + +`--service` overrides `PD_MAPPER_SERVICE`. Ordinary users should leave it empty +so a service and expected domains are selected from live registry files. An +override must name a `provider/service` entry present in those files and should +come from a product or CI requirement. Precedence is CLI, environment, then +dynamic registry discovery. The suite does not maintain per-SoC service lists. + +## Result policy + +- `PASS`: the selected kernel or userspace implementation is ready, QRTR service + `64:1:1` is advertised, registry data is well formed, and the functional QMI + query returns the exact registry-derived domains when its optional Python + transport is available. +- `FAIL`: a kernel auxiliary device is incorrectly bound, a provisioned + userspace service is inactive, the required QRTR advertisement is missing, + topology collection fails, registry data is malformed, the functional reply + is invalid or disagrees with the registry, or relevant kernel errors exist. +- `SKIP`: no kernel device, userspace unit/process, or live protocol + advertisement is present. Missing registry files, Python, both QRTR lookup + providers, or kernel-log access can also skip only the affected subcheck. + +Use `[PD-MAPPER-KERNEL]`, `[PD-MAPPER-AUX]`, `[PD-MAPPER-USERSPACE]`, +`[PD-MAPPER-SELECTION]`, `[PD-MAPPER-QRTR-ENDPOINT]`, +`[PD-MAPPER-REGISTRY-FILE]`, `[PD-MAPPER-FUNCTIONAL]`, +`PD_MAPPER_SELECTION`, `PD_MAPPER_PROBE`, `PD_MAPPER_IO`, and +`PD_MAPPER_FUNCTIONAL` to identify the implementation, endpoint, bounded +candidate set, selected service, QMI packet proof, and returned domains in stdout. +`[PD-MAPPER-POLICY]` confirms the dynamically selected applicability path and +the protocol-defined service tuple. Service status, journal, process, topology, +registry, functional domain report, and kernel evidence is retained under the printed +`results/PD_Mapper_Validation/run-*/` directory. Object lists are bounded in +stdout and report the omitted count plus the complete artifact path when the +limit is exceeded. + +Kernel health capture prefers `dmesg` and falls back to image-provided +`journalctl -k`; `kernel/dmesg_access.log` retains provider and permission +diagnostics. diff --git a/Runner/suites/Kernel/Baseport/PD_Mapper_Validation/run.sh b/Runner/suites/Kernel/Baseport/PD_Mapper_Validation/run.sh new file mode 100755 index 00000000..df49693e --- /dev/null +++ b/Runner/suites/Kernel/Baseport/PD_Mapper_Validation/run.sh @@ -0,0 +1,305 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# ---------- Repo env + helpers ---------- +SCRIPT_DIR="$( + cd "$(dirname "$0")" || exit 1 + pwd +)" +INIT_ENV="" +SEARCH="$SCRIPT_DIR" + +while [ "$SEARCH" != "/" ]; do + if [ -f "$SEARCH/init_env" ]; then + INIT_ENV="$SEARCH/init_env" + break + fi + SEARCH=$(dirname "$SEARCH") +done + +if [ -z "$INIT_ENV" ]; then + echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 + exit 1 +fi + +# Only source once (idempotent) +# NOTE: We intentionally **do not export** any new vars. They stay local to this shell. +if [ -z "${__INIT_ENV_LOADED:-}" ]; then + # shellcheck disable=SC1090 + . "$INIT_ENV" + __INIT_ENV_LOADED=1 +fi + +# shellcheck disable=SC1090 +. "$INIT_ENV" +# shellcheck disable=SC1091 +. "$TOOLS/functestlib.sh" +# shellcheck disable=SC1091 +. "$TOOLS/lib_qrtr.sh" +TESTNAME="PD_Mapper_Validation" +RES_FILE="$SCRIPT_DIR/$TESTNAME.res" + +PD_MAPPER_TIMEOUT="${PD_MAPPER_TIMEOUT:-10}" +PD_MAPPER_SERVICE="${PD_MAPPER_SERVICE:-}" +PD_MAPPER_SERVICE_SOURCE="dynamic-registry" +if [ -n "$PD_MAPPER_SERVICE" ]; then + PD_MAPPER_SERVICE_SOURCE="environment" +fi +RESULT_DIR="$SCRIPT_DIR/results/$TESTNAME/run-$(date '+%Y%m%d-%H%M%S')-$$" +TOPOLOGY_FILE="$RESULT_DIR/qrtr_lookup.log" +KERNEL_REPORT="$RESULT_DIR/pd_mapper_kernel.tsv" +REGISTRY_LIST="$RESULT_DIR/service_registry_files.log" +REGISTRY_REPORT="$RESULT_DIR/service_registry_validation.tsv" +FUNCTIONAL_LOG="$RESULT_DIR/pd_mapper_functional.log" +FUNCTIONAL_REPORT="$RESULT_DIR/pd_mapper_domains.tsv" +PD_MAPPER_CORE_UNVERIFIED=0 + +# usage +# Takes no arguments, prints the supported CLI contract to stdout, and has no +# side effects. +usage() { + printf '%s\n' \ + "Usage: ./run.sh [options]" \ + " --timeout SECONDS Bound runtime probes, default: 10" \ + " --service NAME Select a registry-provided service for the functional query" \ + " -h, --help" \ + "Kernel and userspace PD Mapper applicability, endpoints, and service data are discovered automatically." \ + "CLI options override environment variables." +} + +# parse_args +# Applies option values and selection provenance to suite globals. Produces no +# stdout, returns 0 on success or 2 for a missing or unknown argument, and does +# not probe QRTR or mutate target state. +parse_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + --timeout) + [ "$#" -ge 2 ] || return 2 + PD_MAPPER_TIMEOUT="$2" + shift 2 + ;; + --service) + [ "$#" -ge 2 ] || return 2 + PD_MAPPER_SERVICE="$2" + if [ -n "$PD_MAPPER_SERVICE" ]; then + PD_MAPPER_SERVICE_SOURCE="cli" + else + PD_MAPPER_SERVICE_SOURCE="dynamic-registry" + fi + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + log_error "Unknown argument: $1" + return 2 + ;; + esac + done +} + +parse_args "$@" || { + usage >&2 + exit 2 +} + +test_result_init "$TESTNAME" "$RES_FILE" +if ! mkdir -p "$RESULT_DIR"; then + test_result_finish "FAIL" "$TESTNAME FAIL: cannot create retained evidence directory $RESULT_DIR" +fi + +log_info "--------------------------------------------------------------------------" +log_info "Starting $TESTNAME" +log_info "Evidence directory: $RESULT_DIR" +log_info "PD Mapper validation: selecting the kernel implementation first, falling back to userspace, and querying a discovered service through QMI" +log_info "[PD-MAPPER-POLICY] applicability=dynamic implementation=kernel-first-userspace-fallback service_tuple=64:1:1 qmi_version=0x101 source=public-protocol timeout=${PD_MAPPER_TIMEOUT}s functional_service=${PD_MAPPER_SERVICE:-auto} service_source=$PD_MAPPER_SERVICE_SOURCE" + +case "$PD_MAPPER_TIMEOUT" in + ''|*[!0-9]*|0) + test_result_record "FAIL" "PD Mapper configuration is invalid, timeout must be a positive integer" + test_result_finish + ;; +esac + +pd_mapper_capture_kernel_runtime "$KERNEL_REPORT" +pd_mapper_log_kernel_runtime "$KERNEL_REPORT" 32 + +qrtr_service_discover pd-mapper.service pd-mapper pd-mapper +log_info "[PD-MAPPER-USERSPACE] fallback_candidate=yes unit=pd-mapper.service unit_exists=$QRTR_SERVICE_UNIT_EXISTS active=$QRTR_SERVICE_ACTIVE pids=${QRTR_SERVICE_PIDS:-none} binary=${QRTR_SERVICE_BINARY_PATH:-not-found}" +if [ "$PD_MAPPER_AUX_COUNT" -eq 0 ] && [ "$QRTR_SERVICE_APPLICABLE" -eq 1 ]; then + qrtr_capture_service_evidence \ + pd-mapper.service \ + pd-mapper \ + "$RESULT_DIR/service" \ + "$PD_MAPPER_TIMEOUT" + log_file_with_label "PD-MAPPER-SERVICE-STATUS" "$RESULT_DIR/service/systemd-status.log" 15 + log_file_with_label "PD-MAPPER-PROCESS" "$RESULT_DIR/service/process.log" 10 +fi + +pd_mapper_tuple_present=0 +qrtr_capture_topology "$TOPOLOGY_FILE" "$PD_MAPPER_TIMEOUT" +topology_rc=$? +if [ "$topology_rc" -eq 0 ]; then + log_info "[PD-MAPPER-QRTR] lookup_provider=$QRTR_LOOKUP_PROVIDER command=$QRTR_LOOKUP_COMMAND artifact=$TOPOLOGY_FILE" +fi +case "$topology_rc" in + 0) + if qrtr_topology_has_service "$TOPOLOGY_FILE" 64 1 1; then + pd_mapper_tuple_present=1 + log_info "[PD-MAPPER-QRTR] expected=64:1:1 qmi_version=0x101 observed=present artifact=$TOPOLOGY_FILE" + qrtr_log_service_matches "$TOPOLOGY_FILE" 64 1 1 "PD-MAPPER-QRTR-ENDPOINT" + else + log_fail "[PD-MAPPER-QRTR] expected=64:1:1 qmi_version=0x101 observed=missing artifact=$TOPOLOGY_FILE" + fi + ;; + 2) + log_info "[PD-MAPPER-QRTR] expected=64:1:1 qmi_version=0x101 observed=unavailable reason=qrtr-runtime-or-lookup-unavailable" + ;; + *) + log_fail "[PD-MAPPER-QRTR] expected=64:1:1 qmi_version=0x101 observed=query-failed rc=$topology_rc artifact=$TOPOLOGY_FILE" + log_file_with_label "PD-MAPPER-QRTR-RAW" "$TOPOLOGY_FILE" 25 + ;; +esac + +if [ "$PD_MAPPER_AUX_COUNT" -gt 0 ]; then + PD_MAPPER_IMPLEMENTATION="kernel" + log_info "[PD-MAPPER-SELECTION] implementation=kernel source=runtime-auxiliary-device userspace_fallback=not-selected" +elif [ "$QRTR_SERVICE_APPLICABLE" -eq 1 ]; then + PD_MAPPER_IMPLEMENTATION="userspace" + log_info "[PD-MAPPER-SELECTION] implementation=userspace source=service-or-process-discovery reason=no-kernel-auxiliary-device" +elif [ "$pd_mapper_tuple_present" -eq 1 ]; then + PD_MAPPER_IMPLEMENTATION="protocol-only" + log_info "[PD-MAPPER-SELECTION] implementation=unattributed source=live-qrtr-advertisement reason=no-kernel-auxiliary-or-userspace-owner-evidence" +else + PD_MAPPER_IMPLEMENTATION="none" +fi + +if [ "$PD_MAPPER_IMPLEMENTATION" = "none" ]; then + test_result_record "SKIP" "No runtime kernel PD Mapper auxiliary device, userspace service or process, or service 64:1:1 advertisement was discovered, config=$PD_MAPPER_KERNEL_CONFIG driver=$PD_MAPPER_DRIVER_STATE binary=${QRTR_SERVICE_BINARY_PATH:-not-found}" + test_result_finish +fi + +if [ "$PD_MAPPER_IMPLEMENTATION" = "kernel" ]; then + if [ "$PD_MAPPER_UNBOUND_COUNT" -gt 0 ] || + [ "$PD_MAPPER_WRONG_DRIVER_COUNT" -gt 0 ]; then + test_result_record "FAIL" "Kernel PD Mapper exposes $PD_MAPPER_AUX_COUNT auxiliary device(s), unbound=$PD_MAPPER_UNBOUND_COUNT wrong_driver=$PD_MAPPER_WRONG_DRIVER_COUNT artifact=$KERNEL_REPORT" + else + test_result_record "PASS" "All $PD_MAPPER_BOUND_COUNT kernel PD Mapper auxiliary device(s) are bound to the registered qcom-pdm-mapper auxiliary driver" + fi +fi + +if [ "$PD_MAPPER_IMPLEMENTATION" = "userspace" ]; then + if [ "$QRTR_SERVICE_ACTIVE" -eq 1 ]; then + test_result_record "PASS" "Userspace PD Mapper fallback is active, pids=${QRTR_SERVICE_PIDS:-systemd-confirmed} binary=${QRTR_SERVICE_BINARY_PATH:-not-in-path}" + else + test_result_record "FAIL" "Userspace PD Mapper is provisioned but inactive, unit_exists=$QRTR_SERVICE_UNIT_EXISTS binary=${QRTR_SERVICE_BINARY_PATH:-not-found} status_artifact=$RESULT_DIR/service/systemd-status.log" + fi +fi + +if [ "$pd_mapper_tuple_present" -eq 1 ]; then + test_result_record "PASS" "PD Mapper advertises QRTR service 64 version 1 instance 1, representing QMI version 0x101" +elif [ "$PD_MAPPER_IMPLEMENTATION" != "protocol-only" ] && [ "$topology_rc" -eq 2 ]; then + test_result_record "SKIP" "PD Mapper runtime is present but neither QRTR lookup provider nor QRTR runtime evidence is available" + PD_MAPPER_CORE_UNVERIFIED=1 +elif [ "$PD_MAPPER_IMPLEMENTATION" != "protocol-only" ]; then + test_result_record "FAIL" "$PD_MAPPER_IMPLEMENTATION PD Mapper is present but does not advertise QRTR service 64 version 1 instance 1 for QMI version 0x101" +fi + +pd_mapper_capture_registry_files "$REGISTRY_LIST" +pd_mapper_registry_valid=0 +if pd_mapper_validate_registry_files \ + "$REGISTRY_LIST" \ + "$REGISTRY_REPORT" \ + "$PD_MAPPER_TIMEOUT"; then + log_info "[PD-MAPPER-REGISTRY] files=$PD_MAPPER_REGISTRY_COUNT validated=$PD_MAPPER_REGISTRY_VALIDATED_COUNT validator=$PD_MAPPER_REGISTRY_VALIDATOR list=$REGISTRY_LIST report=$REGISTRY_REPORT" + log_file_with_label "PD-MAPPER-REGISTRY-FILE" "$REGISTRY_REPORT" 32 + if [ "$PD_MAPPER_REGISTRY_COUNT" -eq 0 ]; then + test_result_record "SKIP" "No service-registry .jsn files were discovered from running remoteproc firmware paths" + elif [ "$PD_MAPPER_REGISTRY_VALIDATOR" = "unavailable" ]; then + test_result_record "SKIP" "$PD_MAPPER_REGISTRY_COUNT service-registry file(s) were discovered but no image-provided JSON validator is available" + else + test_result_record "PASS" "Validated $PD_MAPPER_REGISTRY_VALIDATED_COUNT service-registry file(s)" + pd_mapper_registry_valid=1 + fi +else + log_fail "[PD-MAPPER-REGISTRY] files=$PD_MAPPER_REGISTRY_COUNT invalid=$PD_MAPPER_REGISTRY_INVALID_COUNT report=$REGISTRY_REPORT" + log_file_with_label "PD-MAPPER-REGISTRY-FILE" "$REGISTRY_REPORT" 32 + test_result_record "FAIL" "$PD_MAPPER_REGISTRY_INVALID_COUNT discovered service-registry file(s) are malformed" +fi + +pd_mapper_functional_ready=0 +if [ "$pd_mapper_tuple_present" -eq 1 ] && + [ "$pd_mapper_registry_valid" -eq 1 ]; then + pd_mapper_functional_ready=1 +elif [ "$pd_mapper_tuple_present" -eq 1 ] && + [ "$PD_MAPPER_IMPLEMENTATION" = "kernel" ] && + [ "$PD_MAPPER_REGISTRY_COUNT" -eq 0 ]; then + pd_mapper_functional_ready=1 +fi + +if [ "$pd_mapper_functional_ready" -eq 1 ]; then + endpoint=$(qrtr_find_service_endpoint "$TOPOLOGY_FILE" 64 1 1 2>/dev/null) + endpoint_rc=$? + if [ "$endpoint_rc" -eq 2 ]; then + log_info "[PD-MAPPER-FUNCTIONAL] phase=selection action=skip reason=multiple-service-endpoints" + qrtr_log_service_matches "$TOPOLOGY_FILE" 64 1 1 "PD-MAPPER-FUNCTIONAL-CANDIDATE" + test_result_record "SKIP" "Multiple PD Mapper endpoints are advertised, the functional client will not select one by enumeration order" + elif [ "$endpoint_rc" -ne 0 ]; then + test_result_record "FAIL" "PD Mapper endpoint could not be resolved from the validated topology, rc=$endpoint_rc" + elif ! command -v python3 >/dev/null 2>&1; then + test_result_record "SKIP" "PD Mapper functional query requires image-provided Python" + else + functional_node=$(printf '%s\n' "$endpoint" | awk '{ print $1 }') + functional_port=$(printf '%s\n' "$endpoint" | awk '{ print $2 }') + functional_outer_timeout=$((PD_MAPPER_TIMEOUT + 5)) + log_info "[PD-MAPPER-FUNCTIONAL] phase=start implementation=$PD_MAPPER_IMPLEMENTATION operation=get-domain-list message_id=0x21 service=${PD_MAPPER_SERVICE:-auto} service_source=$PD_MAPPER_SERVICE_SOURCE node=$functional_node port=$functional_port protocol_timeout=${PD_MAPPER_TIMEOUT}s watchdog=${functional_outer_timeout}s" + run_with_timeout_log \ + "$functional_outer_timeout" \ + "$FUNCTIONAL_LOG" \ + python3 "$TOOLS/pd_mapper_client.py" \ + --node "$functional_node" \ + --port "$functional_port" \ + --registry-list "$REGISTRY_LIST" \ + --report-file "$FUNCTIONAL_REPORT" \ + --service "$PD_MAPPER_SERVICE" \ + --implementation "$PD_MAPPER_IMPLEMENTATION" \ + --timeout "$PD_MAPPER_TIMEOUT" + functional_rc=$? + log_file_with_label "PD-MAPPER-FUNCTIONAL" "$FUNCTIONAL_LOG" 25 + log_file_with_label "PD-MAPPER-DOMAIN" "$FUNCTIONAL_REPORT" 32 + if [ "$functional_rc" -eq 0 ]; then + test_result_record "PASS" "PD Mapper completed a QMI get-domain-list request with validated domain data, implementation=$PD_MAPPER_IMPLEMENTATION report=$FUNCTIONAL_REPORT" + elif [ "$functional_rc" -eq 2 ]; then + test_result_record "SKIP" "The PD Mapper functional client found no selectable registry service" + else + test_result_record "FAIL" "PD Mapper QMI get-domain-list functional validation failed, implementation=$PD_MAPPER_IMPLEMENTATION rc=$functional_rc artifact=$FUNCTIONAL_LOG" + fi + fi +elif [ "$pd_mapper_tuple_present" -eq 1 ]; then + test_result_record "SKIP" "PD Mapper functional query needs at least one validated runtime service-registry file to derive a portable service and expected domain set" +fi + +export KERNEL_LOG_JOURNAL_FALLBACK=1 +scan_dmesg_errors \ + "$RESULT_DIR/kernel" \ + 'pd-mapper|servreg|qrtr|qcom_glink|glink|rpmsg' \ + 'endpoint is not connected' +dmesg_rc=$? +if [ "${DMESG_ACCESS_STATUS:-unavailable}" != "available" ]; then + test_result_record "SKIP" "Kernel log access is unavailable for PD Mapper health validation, status=${DMESG_ACCESS_STATUS:-unknown} provider=${DMESG_ACCESS_PROVIDER:-none} rc=${DMESG_ACCESS_RC:-unknown} artifact=$RESULT_DIR/kernel/dmesg_access.log" +elif [ "$dmesg_rc" -eq 0 ]; then + log_file_with_label "PD-MAPPER-KERNEL-ERROR" "$RESULT_DIR/kernel/dmesg_errors.log" 25 + test_result_record "FAIL" "PD Mapper or QRTR kernel errors were detected, artifact=$RESULT_DIR/kernel/dmesg_errors.log" +else + test_result_record "PASS" "No persistent PD Mapper or QRTR kernel errors were found" +fi + +if [ "$TEST_RESULT_FAIL_COUNT" -eq 0 ] && [ "$PD_MAPPER_CORE_UNVERIFIED" -eq 1 ]; then + test_result_finish "SKIP" "$TESTNAME SKIP: PD Mapper is present but its required QRTR service advertisement could not be verified" +fi + +test_result_finish diff --git a/Runner/utils/pd_mapper_client.py b/Runner/utils/pd_mapper_client.py new file mode 100755 index 00000000..1004c2b9 --- /dev/null +++ b/Runner/utils/pd_mapper_client.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +"""Query the Qualcomm PD Mapper get-domain-list QMI operation over QRTR.""" + +import argparse +import hashlib +import json +import lzma +import socket +import struct +import sys +from pathlib import Path +from typing import Dict, List, Optional, Set, Tuple + + +QMI_REQUEST = 0 +QMI_RESPONSE = 2 +SERVREG_GET_DOMAIN_LIST = 0x21 +QMI_HEADER = struct.Struct(" None: + """Print one machine-readable final status line without side effects.""" + details = " ".join(f"{key}={value}" for key, value in fields.items()) + print(f"PD_MAPPER_FUNCTIONAL status={status} reason={reason} {details}".rstrip()) + + +def load_registry(list_file: Path) -> Dict[str, Set[Tuple[str, int]]]: + """Return registry service names mapped to their expected domain tuples.""" + services: Dict[str, Set[Tuple[str, int]]] = {} + for raw_path in list_file.read_text(encoding="utf-8").splitlines(): + if not raw_path: + continue + path = Path(raw_path) + opener = lzma.open if path.name.endswith(".xz") else open + with opener(path, "rt", encoding="utf-8") as stream: + root = json.load(stream) + domain_data = root["sr_domain"] + domain = "/".join( + (domain_data["soc"], domain_data["domain"], domain_data["subdomain"]) + ) + instance = int(domain_data["qmi_instance_id"]) + for entry in root["sr_service"]: + service = f'{entry["provider"]}/{entry["service"]}' + services.setdefault(service, set()).add((domain, instance)) + return services + + +def parse_tlvs(payload: bytes) -> Dict[int, bytes]: + """Decode one QMI payload into unique TLVs or raise on malformed input.""" + tlvs: Dict[int, bytes] = {} + offset = 0 + while offset < len(payload): + if len(payload) - offset < 3: + raise ValueError(f"truncated-tlv-header-at-{offset}") + tlv_type, tlv_length = struct.unpack_from(" len(payload): + raise ValueError(f"truncated-tlv-value-type-{tlv_type}") + if tlv_type in tlvs: + raise ValueError(f"duplicate-tlv-type-{tlv_type}") + tlvs[tlv_type] = payload[offset:end] + offset = end + return tlvs + + +def decode_domains(value: bytes) -> List[Tuple[str, int]]: + """Decode a PD Mapper domain-list TLV into domain and instance tuples.""" + if not value: + raise ValueError("empty-domain-list-tlv") + count = value[0] + offset = 1 + domains: List[Tuple[str, int]] = [] + for index in range(count): + if offset >= len(value): + raise ValueError(f"truncated-domain-name-length-at-{index}") + name_length = value[offset] + offset += 1 + fixed_length = name_length + 9 + if len(value) - offset < fixed_length: + raise ValueError(f"truncated-domain-entry-at-{index}") + name_bytes = value[offset : offset + name_length] + offset += name_length + instance, service_data_valid, service_data = struct.unpack_from( + " Tuple[List[Tuple[str, int]], int, bytes, bytes]: + """Send one bounded QMI page request and return validated response data.""" + service_bytes = service.encode("utf-8") + if not service_bytes or len(service_bytes) > 256 or b"\0" in service_bytes: + raise ValueError(f"invalid-service-name-length-{len(service_bytes)}") + payload = struct.pack(" str: + """Return a bounded hexadecimal packet preview for diagnostic logging.""" + return payload[:32].hex() or "empty" + + +def query_service( + client: socket.socket, + endpoint: Tuple[int, int], + service: str, + transaction_start: int, +) -> Tuple[Set[Tuple[str, int]], List[bytes], List[bytes], int]: + """Query all bounded pages for one service and return domains and packets.""" + observed: Set[Tuple[str, int]] = set() + request_packets: List[bytes] = [] + response_packets: List[bytes] = [] + offset = 0 + transaction = transaction_start + total = 0 + while True: + domains, total, request, response = request_domains( + client, + endpoint, + service, + transaction, + offset if offset else None, + ) + request_packets.append(request) + response_packets.append(response) + observed.update(domains) + if len(observed) >= total: + break + if not domains: + raise ValueError(f"pagination-stalled-offset-{offset}-total-{total}") + offset += len(domains) + transaction = (transaction + 1) & 0xFFFF or 1 + if len(request_packets) > 16: + raise ValueError("pagination-limit-exceeded") + return observed, request_packets, response_packets, transaction + + +def main() -> int: + """Run CLI validation and return 0 for PASS, 1 for FAIL, or 2 for SKIP.""" + parser = argparse.ArgumentParser() + parser.add_argument("--node", type=int, required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--registry-list", type=Path, required=True) + parser.add_argument("--report-file", type=Path, required=True) + parser.add_argument("--service", default="") + parser.add_argument( + "--implementation", + choices=("kernel", "userspace", "protocol-only"), + required=True, + ) + parser.add_argument("--timeout", type=float, required=True) + args = parser.parse_args() + + family = getattr(socket, "AF_QIPCRTR", 42) + + try: + services = load_registry(args.registry_list) + except (OSError, EOFError, KeyError, TypeError, ValueError, json.JSONDecodeError) as error: + emit("FAIL", "registry-read-or-parse", message=repr(str(error))) + return 1 + if not services and (args.implementation != "kernel" or args.service): + emit("SKIP", "no-registry-services") + return 2 + + if args.service and args.service not in services: + emit( + "FAIL", + "selected-service-not-in-registry", + service=args.service, + available_services=len(services), + ) + return 1 + + service_source = "override" if args.service else "dynamic-registry" + candidates = [args.service] if args.service else sorted(services)[:32] + omitted_candidates = max(len(services) - len(candidates), 0) + print( + "PD_MAPPER_SELECTION" + f" source={service_source} available_services={len(services)}" + f" candidates={len(candidates)} omitted={omitted_candidates}" + f" registry_list={args.registry_list}" + ) + endpoint = (args.node, args.port) + service = "" + expected: Optional[Set[Tuple[str, int]]] = None + observed: Set[Tuple[str, int]] = set() + all_requests: List[bytes] = [] + all_responses: List[bytes] = [] + transaction = 1 + active_service = "none" + + try: + with socket.socket(family, socket.SOCK_DGRAM) as client: + client.settimeout(args.timeout) + for candidate in candidates: + active_service = candidate + candidate_domains, requests, responses, transaction = query_service( + client, endpoint, candidate, transaction + ) + all_requests.extend(requests) + all_responses.extend(responses) + print( + "PD_MAPPER_PROBE" + f" service={candidate} source={service_source}" + f" observed_domains={len(candidate_domains)}" + ) + transaction = (transaction + 1) & 0xFFFF or 1 + if candidate_domains: + service = candidate + expected = services[candidate] + observed = candidate_domains + break + + if not service and args.implementation == "kernel" and not args.service: + service = "tms/servreg" + active_service = service + service_source = "public-kernel-contract" + observed, requests, responses, transaction = query_service( + client, endpoint, service, transaction + ) + del transaction + all_requests.extend(requests) + all_responses.extend(responses) + print( + "PD_MAPPER_PROBE" + f" service={service} source={service_source}" + f" observed_domains={len(observed)}" + ) + except socket.timeout: + emit( + "FAIL", + "response-timeout", + service=active_service, + node=args.node, + port=args.port, + received_domains=len(observed), + ) + return 1 + except (OSError, UnicodeDecodeError, ValueError, RuntimeError) as error: + emit( + "FAIL", + "protocol-error", + service=active_service, + message=repr(str(error)), + ) + return 1 + + if not service: + emit( + "FAIL", + "no-registry-service-resolved", + implementation=args.implementation, + candidates=len(candidates), + ) + return 1 + if not observed: + emit( + "FAIL", + "empty-domain-list", + implementation=args.implementation, + service=service, + service_source=service_source, + ) + return 1 + + args.report_file.parent.mkdir(parents=True, exist_ok=True) + with args.report_file.open("w", encoding="utf-8", newline="\n") as report: + report.write("state\tdomain\tinstance\n") + expected_for_report = expected or set() + for domain, instance in sorted(expected_for_report | observed): + if expected is None: + state = "observed" + elif (domain, instance) in expected and (domain, instance) in observed: + state = "matched" + elif (domain, instance) in expected: + state = "missing" + else: + state = "unexpected" + report.write(f"{state}\t{domain}\t{instance}\n") + + request_data = b"".join(all_requests) + response_data = b"".join(all_responses) + print( + "PD_MAPPER_IO direction=tx" + f" packets={len(all_requests)} bytes={len(request_data)}" + f" sha256={hashlib.sha256(request_data).hexdigest()}" + f" preview_hex={preview(request_data)}" + ) + print( + "PD_MAPPER_IO direction=rx" + f" packets={len(all_responses)} bytes={len(response_data)}" + f" sha256={hashlib.sha256(response_data).hexdigest()}" + f" preview_hex={preview(response_data)}" + ) + if expected is not None and observed != expected: + emit( + "FAIL", + "domain-set-mismatch", + service=service, + service_source=service_source, + expected_domains=len(expected), + observed_domains=len(observed), + report=args.report_file, + ) + return 1 + + emit( + "PASS", + "domain-list-verified", + service=service, + service_source=service_source, + node=args.node, + port=args.port, + domains=len(observed), + comparison="exact-registry" if expected is not None else "nonempty-structural", + report=args.report_file, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())