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/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/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/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/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" +} 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()) 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()) 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())