From db9b05433a464018feb8898a39a761669c998ec4 Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Sat, 25 Jul 2026 05:58:20 +0530 Subject: [PATCH 1/4] audio: add Debian audio runtime infrastructure Add the shared package, device-permission, user-session, and WAV validation support required by the audio test runners. - add Debian package mappings for base and AudioReach validation - add the AudioReach dma-heap udev permission rule - prepare the Debian audio user and optional systemd user manager - run individual audio commands and helpers through the Debian user - validate dma-heap and ALSA node access without re-executing runners - manage PipeWire user services from the root orchestrator - centralize recording workspace and backend recovery helpers - add WAV structure and sample-content validation - preserve native Yocto and qcom-distro behavior Signed-off-by: Srikanth Muppandam --- Runner/config/pkg_command_map.conf | 33 + Runner/config/udev/90-qcom-audioreach.rules | 10 + Runner/utils/audio_common.sh | 3182 ++++++++++++++++++- Runner/utils/audio_wav_validate.py | 506 +++ 4 files changed, 3694 insertions(+), 37 deletions(-) create mode 100755 Runner/config/udev/90-qcom-audioreach.rules create mode 100755 Runner/utils/audio_wav_validate.py diff --git a/Runner/config/pkg_command_map.conf b/Runner/config/pkg_command_map.conf index 764c24179..2667e0b36 100755 --- a/Runner/config/pkg_command_map.conf +++ b/Runner/config/pkg_command_map.conf @@ -188,3 +188,36 @@ debian:package-set:graphics-base=libegl-mesa0 libgl1-mesa-dri libgbm1 libegl1 li # TODO on ubuntu ubuntu:package-set:graphics=kgsl-dkms adreno-common adreno-gles1 adreno-gles2 adreno-egl1 adreno-vulkan-icd adreno-opencl-icd adreno-opencl-dev + +# --------------------------------------------------------------------------- +# Audio validation package recovery. +# +# Debian Audio tests use the upstream base Audio stack by default. +# Qualcomm AudioReach packages are installed only when --overlay is explicitly +# requested. +# +# No rpm, opkg, qcom-distro, or generic provider package-set mapping is +# provided. Yocto and other embedded images therefore continue using their +# image-provided Audio stack without package recovery. +# --------------------------------------------------------------------------- + +# Base Debian Audio stack. +debian:package-set:audio-base=alsa-utils pipewire-bin wireplumber pulseaudio-utils + +# Debian Qualcomm AudioReach overlay stack. +# +# This package set is complete by itself because overlay mode checks only +# "audio"; it does not check "audio-base" first. +debian:package-set:audio=alsa-utils pipewire-bin wireplumber pulseaudio-utils audioreach-pipewire-plugin audioreach-kernel-dkms audioreach-config + +# Command recovery used by check_dependencies()/pkg_ensure_command(). +apt:aplay=alsa-utils +apt:arecord=alsa-utils +apt:amixer=alsa-utils +apt:alsaucm=alsa-utils +apt:wpctl=wireplumber +apt:pw-play=pipewire-bin +apt:pw-record=pipewire-bin +apt:pactl=pulseaudio-utils +apt:paplay=pulseaudio-utils +apt:parecord=pulseaudio-utils diff --git a/Runner/config/udev/90-qcom-audioreach.rules b/Runner/config/udev/90-qcom-audioreach.rules new file mode 100755 index 000000000..decfac89e --- /dev/null +++ b/Runner/config/udev/90-qcom-audioreach.rules @@ -0,0 +1,10 @@ +# Qualcomm AudioReach runtime permissions. +# +# Audio tests execute as the Debian audio user rather than root. The dma-heap +# device therefore needs group read/write access through the standard audio +# group. +# +# The test automation installs this rule into /run/udev/rules.d on applicable +# Debian AudioReach overlay runs. + +SUBSYSTEM=="dma_heap", KERNEL=="system", OWNER="root", GROUP="audio", MODE="0660" diff --git a/Runner/utils/audio_common.sh b/Runner/utils/audio_common.sh index 50417481c..65fa08c17 100755 --- a/Runner/utils/audio_common.sh +++ b/Runner/utils/audio_common.sh @@ -3,6 +3,13 @@ # SPDX-License-Identifier: BSD-3-Clause # Common audio helpers for PipeWire / PulseAudio runners. # Requires: functestlib.sh (log_* helpers, extract_tar_from_url, scan_dmesg_errors) +# Reuse the repository package-provider abstraction directly. +if [ -n "${TOOLS:-}" ] && + [ -r "$TOOLS/lib_pkg_provider.sh" ] && + ! command -v pkg_detect_os_id >/dev/null 2>&1; then + # shellcheck disable=SC1091 + . "$TOOLS/lib_pkg_provider.sh" +fi # Check whether a command exists in PATH. # Used by bootstrap helpers before attempting backend startup. @@ -420,49 +427,306 @@ audio_restart_services_best_effort() { return 0 } -# Restart PipeWire through systemd without blocking forever in `systemctl restart`. -# Polls the unit state until the restart job settles or times out. +# Run PipeWire systemctl operations in the correct service scope. +# +# Debian: +# Root orchestrator: +# runuser -u debian -- systemctl --user ... +# +# Temporary migration compatibility: +# Existing runners that are still fully re-executed as the Debian Audio +# user call systemctl --user directly. +# +# Yocto/qcom-distro/other: +# systemctl ... +# +# Args: +# All arguments are passed unchanged to systemctl. +# +# Returns: +# The original systemctl exit status. +# 1 when the required user-session environment is unavailable. +audio_pipewire_systemctl() { + if [ "$#" -eq 0 ]; then + log_fail "audio_pipewire_systemctl requires systemctl arguments" + return 1 + fi + + if ! command -v systemctl >/dev/null 2>&1; then + log_fail "systemctl is unavailable" + return 1 + fi + + ########################################################################### + # Platform detection + ########################################################################### + + if command -v pkg_detect_os_id >/dev/null 2>&1; then + aps_os_id="$( + pkg_detect_os_id 2>/dev/null || + echo unknown + )" + else + aps_os_id="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" + fi + + [ -n "$aps_os_id" ] || aps_os_id="unknown" + + case "$aps_os_id" in + debian) + ;; + *) + # Preserve existing native/Yocto system-service behavior. + systemctl "$@" + return $? + ;; + esac + + ########################################################################### + # Preferred Debian path: root orchestrator, command-level user execution + ########################################################################### + + aps_current_uid="$(id -u 2>/dev/null || echo 1)" + + if [ "$aps_current_uid" -eq 0 ] 2>/dev/null; then + if ! command -v audio_run_as_test_user >/dev/null 2>&1; then + log_fail "Audio user-command helper is unavailable" + return 1 + fi + + audio_run_as_test_user \ + --require-session \ + systemctl --user "$@" + + return $? + fi + + ########################################################################### + # Temporary compatibility for runners not migrated away from full re-exec + ########################################################################### + + case "${AUDIO_TEST_USER_REEXEC:-0}:${AUDIO_TEST_COMMAND_USER_CONTEXT:-0}" in + 1:*|*:1) + ;; + *) + log_fail "Debian PipeWire systemctl operation was invoked outside the prepared Audio user context" + log_fail "Run audio_prepare_debian_audio_environment before invoking PipeWire service operations" + return 1 + ;; + esac + + aps_expected_user="${AUDIO_TEST_USER:-debian}" + aps_current_user="$(id -un 2>/dev/null || echo unknown)" + + if [ "$aps_current_user" != "$aps_expected_user" ]; then + log_fail "Unexpected Debian PipeWire service user: current=$aps_current_user expected=$aps_expected_user" + return 1 + fi + + if [ -z "${XDG_RUNTIME_DIR:-}" ]; then + log_fail "XDG_RUNTIME_DIR is not set for PipeWire user-service access" + return 1 + fi + + if [ ! -d "$XDG_RUNTIME_DIR" ]; then + log_fail "PipeWire user runtime directory is unavailable: $XDG_RUNTIME_DIR" + return 1 + fi + + if [ ! -S "$XDG_RUNTIME_DIR/bus" ]; then + log_fail "PipeWire user D-Bus socket is unavailable: $XDG_RUNTIME_DIR/bus" + return 1 + fi + + if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then + DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus" + export DBUS_SESSION_BUS_ADDRESS + fi + + systemctl --user "$@" + return $? +} + +# Restart PipeWire through systemd and wait for the service state to settle. +# +# Service scope: +# AUDIO_SYSTEMCTL_USER_SCOPE=1 +# Use the current user's PipeWire service: +# systemctl --user ... +# +# AUDIO_SYSTEMCTL_USER_SCOPE unset or 0 +# Preserve the existing system-service behavior: +# systemctl ... +# +# Args: +# $1 - attempt label used in logs, for example 1/1 +# +# Environment: +# PIPEWIRE_SYSTEMCTL_TIMEOUT +# Maximum number of seconds to wait; default: 180 +# +# Returns: +# 0 - PipeWire reached active/running state +# 1 - restart failed, service failed, or timeout expired audio_restart_pipewire_service() { - aprs_label="$1" + aprs_label="${1:-1/1}" aprs_timeout="${PIPEWIRE_SYSTEMCTL_TIMEOUT:-180}" aprs_start_s="$(date +%s 2>/dev/null || echo 0)" aprs_next_log=10 - - if [ -z "$aprs_label" ]; then - aprs_label="1/1" + aprs_scope="system" + aprs_exec_text="systemctl restart pipewire" + aprs_output_file="${TMPDIR:-/tmp}/audio-pipewire-systemctl.$$.log" + + case "${AUDIO_SYSTEMCTL_USER_SCOPE:-0}" in + 0) + ;; + 1) + aprs_scope="user" + aprs_exec_text="systemctl --user restart pipewire" + ;; + *) + log_fail "Invalid AUDIO_SYSTEMCTL_USER_SCOPE value: ${AUDIO_SYSTEMCTL_USER_SCOPE:-}" + return 1 + ;; + esac + + if ! command -v audio_pipewire_systemctl >/dev/null 2>&1; then + log_fail "PipeWire systemctl scope helper is unavailable" + return 1 fi - + if ! command -v systemctl >/dev/null 2>&1; then - log_fail "systemctl not available, cannot restart pipewire" + log_fail "systemctl is unavailable; cannot restart PipeWire" return 1 fi - - log_info "exec: systemctl restart pipewire (attempt ${aprs_label})" - if ! systemctl restart pipewire >/dev/null 2>&1; then - log_warn "Failed to queue pipewire restart job on attempt ${aprs_label}" + + rm -f "$aprs_output_file" + : >"$aprs_output_file" || { + log_fail "Failed to create PipeWire systemctl output file: $aprs_output_file" return 1 + } + + log_info "exec: $aprs_exec_text (attempt $aprs_label)" + + audio_pipewire_systemctl restart pipewire \ + >"$aprs_output_file" 2>&1 + aprs_restart_rc=$? + + if [ -s "$aprs_output_file" ]; then + while IFS= read -r aprs_line || [ -n "$aprs_line" ]; do + if [ "$aprs_restart_rc" -eq 0 ]; then + log_info "[systemctl:$aprs_scope] $aprs_line" + else + log_warn "[systemctl:$aprs_scope] $aprs_line" + fi + done <"$aprs_output_file" fi - + + if [ "$aprs_restart_rc" -ne 0 ]; then + log_warn "Failed to queue PipeWire restart job on attempt $aprs_label, scope=$aprs_scope, rc=$aprs_restart_rc" + + log_info "Current PipeWire service status, scope=$aprs_scope:" + + : >"$aprs_output_file" + + audio_pipewire_systemctl status \ + pipewire \ + --no-pager \ + --full \ + >"$aprs_output_file" 2>&1 || true + + if [ -s "$aprs_output_file" ]; then + while IFS= read -r aprs_line || [ -n "$aprs_line" ]; do + log_info "[systemctl-status:$aprs_scope] $aprs_line" + done <"$aprs_output_file" + else + log_info "[systemctl-status:$aprs_scope] No status output available" + fi + + if command -v journalctl >/dev/null 2>&1; then + log_info "Recent PipeWire service journal, scope=$aprs_scope:" + + : >"$aprs_output_file" + + if [ "$aprs_scope" = "user" ]; then + journalctl \ + --user \ + -u pipewire \ + -n 30 \ + --no-pager \ + >"$aprs_output_file" 2>&1 || true + else + journalctl \ + -u pipewire \ + -n 30 \ + --no-pager \ + >"$aprs_output_file" 2>&1 || true + fi + + if [ -s "$aprs_output_file" ]; then + while IFS= read -r aprs_line || [ -n "$aprs_line" ]; do + log_info "[journalctl:$aprs_scope] $aprs_line" + done <"$aprs_output_file" + else + log_info "[journalctl:$aprs_scope] No journal entries available" + fi + fi + + rm -f "$aprs_output_file" + return 1 + fi + + rm -f "$aprs_output_file" + while :; do aprs_now_s="$(date +%s 2>/dev/null || echo 0)" aprs_elapsed=$((aprs_now_s - aprs_start_s)) + if [ "$aprs_elapsed" -lt 0 ]; then aprs_elapsed=0 fi - - if [ "$aprs_elapsed" -ge "$aprs_timeout" ]; then - aprs_active_state="$(systemctl show -p ActiveState --value pipewire 2>/dev/null || echo unknown)" - aprs_sub_state="$(systemctl show -p SubState --value pipewire 2>/dev/null || echo unknown)" - aprs_job_state="$(systemctl show -p Job --value pipewire 2>/dev/null || echo unknown)" - log_warn "PipeWire restart attempt ${aprs_label} timed out after ${aprs_timeout}s (state=${aprs_active_state}/${aprs_sub_state}, job=${aprs_job_state})" - return 1 - fi - - aprs_active_state="$(systemctl show -p ActiveState --value pipewire 2>/dev/null || echo unknown)" - aprs_sub_state="$(systemctl show -p SubState --value pipewire 2>/dev/null || echo unknown)" - aprs_result_state="$(systemctl show -p Result --value pipewire 2>/dev/null || echo unknown)" - aprs_job_state="$(systemctl show -p Job --value pipewire 2>/dev/null || echo unknown)" - + + aprs_active_state="$( + audio_pipewire_systemctl show \ + -p ActiveState \ + --value \ + pipewire 2>/dev/null || + echo unknown + )" + + aprs_sub_state="$( + audio_pipewire_systemctl show \ + -p SubState \ + --value \ + pipewire 2>/dev/null || + echo unknown + )" + + aprs_result_state="$( + audio_pipewire_systemctl show \ + -p Result \ + --value \ + pipewire 2>/dev/null || + echo unknown + )" + + aprs_job_state="$( + audio_pipewire_systemctl show \ + -p Job \ + --value \ + pipewire 2>/dev/null || + echo unknown + )" + + [ -n "$aprs_active_state" ] || aprs_active_state="unknown" + [ -n "$aprs_sub_state" ] || aprs_sub_state="unknown" + [ -n "$aprs_result_state" ] || aprs_result_state="unknown" + case "$aprs_job_state" in ""|0) aprs_job_done=1 @@ -471,26 +735,36 @@ audio_restart_pipewire_service() { aprs_job_done=0 ;; esac - - if [ "$aprs_active_state" = "active" ] && [ "$aprs_sub_state" = "running" ] && [ "$aprs_job_done" -eq 1 ]; then + + if [ "$aprs_active_state" = "active" ] && + [ "$aprs_sub_state" = "running" ] && + [ "$aprs_job_done" -eq 1 ]; then + log_pass "PipeWire restart completed, scope=$aprs_scope attempt=$aprs_label" return 0 fi - + if [ "$aprs_active_state" = "failed" ]; then - log_warn "PipeWire entered failed state on attempt ${aprs_label} (state=${aprs_active_state}/${aprs_sub_state}, result=${aprs_result_state})" + log_warn "PipeWire entered failed state on attempt $aprs_label, scope=$aprs_scope state=$aprs_active_state/$aprs_sub_state result=$aprs_result_state" return 1 fi - - if [ "$aprs_result_state" = "failed" ]; then - log_warn "PipeWire restart job failed on attempt ${aprs_label} (state=${aprs_active_state}/${aprs_sub_state}, result=${aprs_result_state})" + + case "$aprs_result_state" in + failed|exit-code|signal|core-dump|timeout|watchdog|resources|protocol) + log_warn "PipeWire restart job failed on attempt $aprs_label, scope=$aprs_scope state=$aprs_active_state/$aprs_sub_state result=$aprs_result_state" + return 1 + ;; + esac + + if [ "$aprs_elapsed" -ge "$aprs_timeout" ]; then + log_warn "PipeWire restart attempt $aprs_label timed out after ${aprs_timeout}s, scope=$aprs_scope state=$aprs_active_state/$aprs_sub_state job=${aprs_job_state:-none}" return 1 fi - + if [ "$aprs_elapsed" -ge "$aprs_next_log" ]; then - log_info "Still waiting for pipewire restart job... (state=${aprs_active_state}/${aprs_sub_state} job=${aprs_job_state} ${aprs_elapsed}s/${aprs_timeout}s)" + log_info "Still waiting for PipeWire restart, scope=$aprs_scope state=$aprs_active_state/$aprs_sub_state job=${aprs_job_state:-none} elapsed=${aprs_elapsed}s/${aprs_timeout}s" aprs_next_log=$((aprs_next_log + 10)) fi - + sleep 1 done } @@ -3016,3 +3290,2837 @@ audio_card_dmesg_scan() { log_warn "scan_dmesg_errors helper not available, skipping audio dmesg scan" fi } + +############################################################################### +# Audio package preparation, overlay activation, and WAV payload validation +############################################################################### + +AUDIO_OVERLAY_PACKAGES_CHANGED=0 +AUDIO_OVERLAY_REBOOT_REQUIRED=0 +AUDIO_WAV_VALIDATION_SUMMARY="" + +# Refresh kernel-module metadata and device state after Debian AudioReach +# package preparation. +# +# Args: +# $1 - 0 for base Audio mode +# 1 when --overlay was explicitly requested +# +# $2 - 0 when the AudioReach package state did not change +# 1 when one or more AudioReach packages changed +# +# Platform behavior: +# Debian overlay: +# - run depmod when the AudioReach package state changed +# - install and activate the repository-owned AudioReach udev rule +# +# Debian base: +# - no-op +# +# Yocto/qcom-distro/other: +# - strict no-op +# +# This helper does not: +# - chmod or chown device nodes directly +# - install packages +# - reload kernel modules +# - restart PipeWire +# +# Returns: +# 0 - refresh completed or not applicable +# 1 - refresh failed +audio_refresh_overlay_devices() { + arod_overlay_requested="${1:-0}" + arod_packages_changed="${2:-0}" + + case "$arod_overlay_requested" in + 0) + return 0 + ;; + 1) + ;; + *) + log_fail "Invalid Audio overlay request value: $arod_overlay_requested" + return 1 + ;; + esac + + case "$arod_packages_changed" in + 0|1) + ;; + *) + log_fail "Invalid Audio package-change value: $arod_packages_changed" + return 1 + ;; + esac + + if command -v pkg_detect_os_id >/dev/null 2>&1; then + arod_os_id="$( + pkg_detect_os_id 2>/dev/null || + echo unknown + )" + else + arod_os_id="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" + fi + + [ -n "$arod_os_id" ] || arod_os_id="unknown" + + case "$arod_os_id" in + debian) + ;; + qcom-distro|poky|openembedded|oe) + log_info "Native image detected; AudioReach device refresh is not required" + return 0 + ;; + *) + log_info "AudioReach device refresh is not enabled for os=$arod_os_id" + return 0 + ;; + esac + + if [ "$(id -u 2>/dev/null || echo 1)" -ne 0 ]; then + log_fail "AudioReach device refresh must run as root" + return 1 + fi + + ########################################################################### + # Kernel module dependency metadata + ########################################################################### + + if [ "$arod_packages_changed" -eq 1 ]; then + if command -v depmod >/dev/null 2>&1; then + log_info "Refreshing kernel module dependency metadata" + + if ! audio_exec_with_timeout \ + 30s \ + depmod -a 2>&1; then + log_fail "Failed to refresh kernel module dependency metadata" + return 1 + fi + + log_pass "Kernel module dependency metadata refreshed" + else + log_warn "depmod is unavailable; kernel module metadata was not refreshed" + fi + else + log_info "AudioReach packages are unchanged; depmod refresh is not required" + fi + + ########################################################################### + # Runtime udev rule and device permissions + ########################################################################### + + if ! command -v audio_prepare_audioreach_udev_rule \ + >/dev/null 2>&1; then + log_fail "AudioReach udev preparation helper is unavailable" + return 1 + fi + + if ! audio_prepare_audioreach_udev_rule \ + "$arod_overlay_requested"; then + log_fail "Failed to install or activate the AudioReach udev rule" + return 1 + fi + + log_pass "AudioReach device refresh completed" + return 0 +} + +# Validate the dma-heap node required by the Debian AudioReach overlay. +# +# The helper is deliberately non-mutating: it never chmods the node and never +# changes users or groups. Packaged udev/group policy must provide access. +# +# Return: +# 0 - node exists and is readable/writable by the current test process +# 1 - node is missing or inaccessible +audio_validate_dma_heap_access() { + avdha_node="${AUDIO_DMA_HEAP_NODE:-/dev/dma_heap/system}" + + if [ ! -e "$avdha_node" ]; then + log_fail "$avdha_node is missing" + return 1 + fi + + if command -v stat >/dev/null 2>&1; then + avdha_mode="$(stat -c '%a' "$avdha_node" 2>/dev/null || echo unknown)" + avdha_owner="$(stat -c '%U:%G' "$avdha_node" 2>/dev/null || echo unknown)" + log_info "$avdha_node mode=$avdha_mode owner=$avdha_owner" + fi + + if [ -r "$avdha_node" ] && [ -w "$avdha_node" ]; then + log_pass "$avdha_node is readable and writable" + return 0 + fi + + log_fail "$avdha_node is present but is not accessible to uid=$(id -u 2>/dev/null || echo unknown)" + log_fail "Fix audioreach-config udev/group policy; the test will not chmod the node" + return 1 +} + +# Prepare and validate the Debian AudioReach overlay runtime. +# +# The preferred execution model keeps the test runner as root and executes only +# user-session or unprivileged device-access commands as the Debian Audio user. +# +# Args: +# $1 - 0 for base Audio mode +# 1 when --overlay was explicitly requested +# +# Prerequisite for the preferred Debian root path: +# audio_prepare_debian_audio_environment 1 +# +# Platform behavior: +# Debian overlay: +# - validate dma-heap access as the Debian Audio user +# - validate ALSA node access as the Debian Audio user +# - validate PipeWire through the Debian user session +# - restart PipeWire only when required +# +# Debian base: +# - no-op +# +# Yocto/qcom-distro/other: +# - strict no-op +# +# Environment: +# AUDIO_OVERLAY_PACKAGES_CHANGED +# 0 - AudioReach packages were already installed +# 1 - AudioReach userspace package state changed +# +# AUDIO_OVERLAY_REBOOT_REQUIRED +# 1 - a DKMS change requires reboot +# +# PIPEWIRE_READY_TIMEOUT +# final PipeWire readiness timeout; default: 120 seconds +# +# Returns: +# 0 - runtime ready or not applicable +# 1 - runtime preparation failed +# 2 - reboot required +audio_prepare_overlay_runtime() { + apor_overlay_requested="${1:-0}" + apor_packages_changed="${AUDIO_OVERLAY_PACKAGES_CHANGED:-0}" + apor_ready_timeout="${PIPEWIRE_READY_TIMEOUT:-120}" + + case "$apor_overlay_requested" in + 0) + return 0 + ;; + 1) + ;; + *) + log_fail "Invalid Audio overlay request value: $apor_overlay_requested" + return 1 + ;; + esac + + case "$apor_packages_changed" in + 0|1) + ;; + *) + log_warn "Invalid AUDIO_OVERLAY_PACKAGES_CHANGED value: $apor_packages_changed" + log_warn "Treating AudioReach package state as changed" + apor_packages_changed=1 + ;; + esac + + case "$apor_ready_timeout" in + ''|*[!0-9]*) + log_warn "Invalid PIPEWIRE_READY_TIMEOUT value: $apor_ready_timeout" + apor_ready_timeout=120 + ;; + esac + + ########################################################################### + # Platform detection + ########################################################################### + + if command -v pkg_detect_os_id >/dev/null 2>&1; then + apor_os_id="$( + pkg_detect_os_id 2>/dev/null || + echo unknown + )" + else + apor_os_id="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" + fi + + [ -n "$apor_os_id" ] || apor_os_id="unknown" + + case "$apor_os_id" in + debian) + ;; + *) + # Preserve native/Yocto behavior. + return 0 + ;; + esac + + ########################################################################### + # Reboot-required guard + ########################################################################### + + case "${AUDIO_OVERLAY_REBOOT_REQUIRED:-0}" in + 1) + log_warn "AudioReach DKMS package changed" + log_warn "A reboot is required before AudioReach validation" + return 2 + ;; + 0|'') + ;; + *) + log_fail "Invalid AUDIO_OVERLAY_REBOOT_REQUIRED value: ${AUDIO_OVERLAY_REBOOT_REQUIRED:-}" + return 1 + ;; + esac + + ########################################################################### + # Determine preferred root mode or temporary re-executed-user mode + ########################################################################### + + apor_current_uid="$(id -u 2>/dev/null || echo 1)" + apor_user_command_mode="" + + if [ "$apor_current_uid" -eq 0 ] 2>/dev/null; then + apor_user_command_mode="root-wrapper" + + if ! command -v audio_run_as_test_user >/dev/null 2>&1; then + log_fail "Audio user-command helper is unavailable" + return 1 + fi + + if [ -z "${AUDIO_TEST_USER:-}" ] || + [ -z "${AUDIO_TEST_UID:-}" ] || + [ -z "${AUDIO_TEST_RUNTIME_DIR:-}" ] || + [ -z "${AUDIO_TEST_DBUS_ADDRESS:-}" ]; then + log_fail "Debian Audio environment has not been prepared" + log_fail "Call audio_prepare_debian_audio_environment 1 before overlay runtime preparation" + return 1 + fi + + log_info "Preparing Debian AudioReach runtime from root orchestration" + else + # Temporary compatibility until Playback and Record stop re-executing the + # complete runner as the Debian user. + case "${AUDIO_TEST_USER_REEXEC:-0}:${AUDIO_TEST_COMMAND_USER_CONTEXT:-0}" in + 1:*|*:1) + apor_user_command_mode="existing-user" + ;; + *) + log_fail "AudioReach runtime preparation must be initiated by root" + return 1 + ;; + esac + + log_info "Preparing Debian AudioReach runtime in temporary re-executed-user mode" + fi + + ########################################################################### + # dma-heap existence and diagnostic information + ########################################################################### + + if [ ! -e /dev/dma_heap/system ]; then + log_fail "/dev/dma_heap/system is missing" + return 1 + fi + + if command -v stat >/dev/null 2>&1; then + apor_dma_mode="$( + stat -c '%a' /dev/dma_heap/system 2>/dev/null || + echo unknown + )" + + apor_dma_owner="$( + stat -c '%U:%G' /dev/dma_heap/system 2>/dev/null || + echo unknown + )" + + log_info "/dev/dma_heap/system mode=$apor_dma_mode owner=$apor_dma_owner" + fi + + ########################################################################### + # Validate dma-heap access as the Debian Audio user + ########################################################################### + + if [ "$apor_user_command_mode" = "root-wrapper" ]; then + # Variables in this command script are intentionally expanded by the child shell. + # shellcheck disable=SC2016 + audio_run_as_test_user \ + sh -c ' + dma_node=$1 + [ -r "$dma_node" ] && [ -w "$dma_node" ] + ' \ + sh \ + /dev/dma_heap/system + + apor_dma_rc=$? + else + if [ -r /dev/dma_heap/system ] && + [ -w /dev/dma_heap/system ]; then + apor_dma_rc=0 + else + apor_dma_rc=1 + fi + fi + + if [ "$apor_dma_rc" -ne 0 ]; then + log_fail "Debian Audio user cannot read and write /dev/dma_heap/system" + return 1 + fi + + log_pass "/dev/dma_heap/system is accessible to the Debian Audio user" + + ########################################################################### + # Validate ALSA nodes as the Debian Audio user + ########################################################################### + + if [ ! -d /dev/snd ]; then + log_fail "/dev/snd is missing" + log_fail "No ALSA device nodes are available" + return 1 + fi + # Variables in this command script are intentionally expanded by the child shell. + # shellcheck disable=SC2016 + apor_alsa_probe=' + alsa_node_found=0 + + for alsa_node in \ + /dev/snd/controlC* \ + /dev/snd/pcmC* + do + [ -e "$alsa_node" ] || continue + + alsa_node_found=1 + + if [ -r "$alsa_node" ] && + [ -w "$alsa_node" ]; then + exit 0 + fi + done + + if [ "$alsa_node_found" -eq 0 ]; then + exit 2 + fi + + exit 1 + ' + + if [ "$apor_user_command_mode" = "root-wrapper" ]; then + audio_run_as_test_user \ + sh -c "$apor_alsa_probe" + + apor_alsa_rc=$? + else + sh -c "$apor_alsa_probe" + apor_alsa_rc=$? + fi + + case "$apor_alsa_rc" in + 0) + log_pass "ALSA device nodes are accessible to the Debian Audio user" + ;; + 2) + log_fail "No ALSA control or PCM device nodes were found under /dev/snd" + return 1 + ;; + *) + log_fail "ALSA control and PCM nodes are not accessible to the Debian Audio user" + + if command -v stat >/dev/null 2>&1; then + for apor_snd_node in /dev/snd/*; do + [ -e "$apor_snd_node" ] || continue + + apor_snd_mode="$( + stat -c '%a' "$apor_snd_node" 2>/dev/null || + echo unknown + )" + + apor_snd_owner="$( + stat -c '%U:%G' "$apor_snd_node" 2>/dev/null || + echo unknown + )" + + log_info "[dev-snd] mode=$apor_snd_mode owner=$apor_snd_owner path=$apor_snd_node" + done + fi + + return 1 + ;; + esac + + ########################################################################### + # PipeWire control-plane probe + ########################################################################### + + apor_pipewire_probe=' + if command -v wpctl >/dev/null 2>&1; then + wpctl status >/dev/null 2>&1 + exit $? + fi + + if command -v pw-cli >/dev/null 2>&1; then + pw-cli info 0 >/dev/null 2>&1 + exit $? + fi + + exit 127 + ' + + apor_pipewire_control_ready=0 + + if [ "$apor_user_command_mode" = "root-wrapper" ]; then + if audio_run_as_test_user \ + --require-session \ + sh -c "$apor_pipewire_probe"; then + apor_pipewire_control_ready=1 + fi + else + if sh -c "$apor_pipewire_probe"; then + apor_pipewire_control_ready=1 + fi + fi + + apor_pipewire_service_ready=0 + + if audio_pipewire_systemctl \ + is-active \ + --quiet \ + pipewire >/dev/null 2>&1; then + apor_pipewire_service_ready=1 + fi + + ########################################################################### + # Preserve a healthy unchanged PipeWire runtime + ########################################################################### + + if [ "$apor_packages_changed" -eq 0 ] && + [ "$apor_pipewire_service_ready" -eq 1 ] && + [ "$apor_pipewire_control_ready" -eq 1 ]; then + log_pass "PipeWire user service and control plane are already ready" + log_info "PipeWire restart is not required" + return 0 + fi + + if [ "$apor_packages_changed" -eq 1 ]; then + log_info "AudioReach userspace package state changed" + log_info "Restarting PipeWire to load updated userspace components" + elif [ "$apor_pipewire_service_ready" -ne 1 ]; then + log_warn "PipeWire user service is not active" + log_warn "Attempting one bounded restart" + else + log_warn "PipeWire user service is active but its control plane is unresponsive" + log_warn "Attempting one bounded restart" + fi + + ########################################################################### + # Bounded PipeWire restart + ########################################################################### + + if ! command -v audio_restart_pipewire_service >/dev/null 2>&1; then + log_fail "Bounded PipeWire restart helper is unavailable" + return 1 + fi + + if ! audio_restart_pipewire_service "1/1"; then + log_fail "Failed to restart the Debian PipeWire user service" + return 1 + fi + + ########################################################################### + # Wait for both the user service and the PipeWire control plane + ########################################################################### + + apor_wait=0 + apor_next_log=10 + + log_info "Waiting for PipeWire user runtime readiness, timeout=${apor_ready_timeout}s" + + while [ "$apor_wait" -lt "$apor_ready_timeout" ]; do + apor_pipewire_service_ready=0 + apor_pipewire_control_ready=0 + + if audio_pipewire_systemctl \ + is-active \ + --quiet \ + pipewire >/dev/null 2>&1; then + apor_pipewire_service_ready=1 + fi + + if [ "$apor_user_command_mode" = "root-wrapper" ]; then + if audio_run_as_test_user \ + --require-session \ + sh -c "$apor_pipewire_probe"; then + apor_pipewire_control_ready=1 + fi + else + if sh -c "$apor_pipewire_probe"; then + apor_pipewire_control_ready=1 + fi + fi + + if [ "$apor_pipewire_service_ready" -eq 1 ] && + [ "$apor_pipewire_control_ready" -eq 1 ]; then + log_pass "Debian AudioReach runtime is ready" + return 0 + fi + + if [ "$apor_wait" -ge "$apor_next_log" ]; then + log_info "Still waiting for PipeWire readiness, service=$apor_pipewire_service_ready control=$apor_pipewire_control_ready ${apor_wait}s/${apor_ready_timeout}s" + apor_next_log=$((apor_next_log + 10)) + fi + + sleep 1 + apor_wait=$((apor_wait + 1)) + done + + ########################################################################### + # Failure diagnostics + ########################################################################### + + log_fail "PipeWire did not become ready within ${apor_ready_timeout}s" + + log_info "Current PipeWire user-service status:" + + audio_pipewire_systemctl \ + status \ + pipewire \ + --no-pager \ + --full 2>&1 | + while IFS= read -r apor_status_line || + [ -n "$apor_status_line" ]; do + log_info "[systemctl:user] $apor_status_line" + done + + if [ "$apor_user_command_mode" = "root-wrapper" ]; then + if command -v audio_run_as_test_user >/dev/null 2>&1; then + log_info "Current PipeWire control-plane status:" + + audio_run_as_test_user \ + --require-session \ + sh -c ' + if command -v wpctl >/dev/null 2>&1; then + exec wpctl status + fi + + if command -v pw-cli >/dev/null 2>&1; then + exec pw-cli info 0 + fi + + exit 127 + ' 2>&1 | + while IFS= read -r apor_status_line || + [ -n "$apor_status_line" ]; do + log_info "[pipewire:user] $apor_status_line" + done + fi + else + log_info "Current PipeWire control-plane status:" + + if command -v wpctl >/dev/null 2>&1; then + wpctl status 2>&1 | + while IFS= read -r apor_status_line || + [ -n "$apor_status_line" ]; do + log_info "[wpctl] $apor_status_line" + done + elif command -v pw-cli >/dev/null 2>&1; then + pw-cli info 0 2>&1 | + while IFS= read -r apor_status_line || + [ -n "$apor_status_line" ]; do + log_info "[pw-cli] $apor_status_line" + done + fi + fi + + return 1 +} + +# Install and activate the repository-owned Qualcomm AudioReach udev rule. +# +# Args: +# $1 - 0 for base Audio mode +# 1 when --overlay was explicitly requested +# +# Platform behavior: +# Debian overlay: +# - installs the rule under /run/udev/rules.d +# - reloads udev rules +# - retriggers the dma_heap subsystem when available +# - validates the resulting device ownership and mode +# +# Debian base: +# - no-op +# +# Yocto/qcom-distro/other: +# - strict no-op +# +# Returns: +# 0 - rule installed and active, device not currently enumerated, or not +# applicable +# 1 - rule installation, activation, or validation failed +audio_prepare_audioreach_udev_rule() { + apar_overlay_requested="${1:-0}" + apar_rule_name="90-qcom-audioreach.rules" + apar_rule_source="" + apar_rule_dir="${AUDIO_UDEV_RUNTIME_RULE_DIR:-/run/udev/rules.d}" + apar_rule_target="$apar_rule_dir/$apar_rule_name" + apar_rule_tmp="${apar_rule_target}.$$" + + case "$apar_overlay_requested" in + 0) + return 0 + ;; + 1) + ;; + *) + log_fail "Invalid Audio overlay request value: $apar_overlay_requested" + return 1 + ;; + esac + + if command -v pkg_detect_os_id >/dev/null 2>&1; then + apar_os_id="$(pkg_detect_os_id 2>/dev/null || echo unknown)" + else + apar_os_id="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" + fi + + [ -n "$apar_os_id" ] || apar_os_id="unknown" + + case "$apar_os_id" in + debian) + ;; + qcom-distro|poky|openembedded|oe) + log_info "Native image detected; AudioReach udev preparation is not required" + return 0 + ;; + *) + log_info "AudioReach udev preparation is not enabled for os=$apar_os_id" + return 0 + ;; + esac + + if [ "$(id -u 2>/dev/null || echo 1)" -ne 0 ]; then + log_fail "AudioReach udev rule preparation must run as root" + return 1 + fi + + if ! command -v udevadm >/dev/null 2>&1; then + log_fail "udevadm is unavailable; cannot activate AudioReach device rules" + return 1 + fi + + if ! command -v stat >/dev/null 2>&1; then + log_fail "stat is unavailable; cannot validate AudioReach device permissions" + return 1 + fi + + if ! getent group audio >/dev/null 2>&1; then + log_fail "Required Debian group does not exist: audio" + return 1 + fi + + # Resolve the repository-owned rule without assuming a particular current + # working directory. + if [ -n "${ROOT_DIR:-}" ] && + [ -r "$ROOT_DIR/config/udev/$apar_rule_name" ]; then + apar_rule_source="$ROOT_DIR/config/udev/$apar_rule_name" + elif [ -n "${TOOLS:-}" ] && + [ -r "$TOOLS/../config/udev/$apar_rule_name" ]; then + apar_rule_source="$TOOLS/../config/udev/$apar_rule_name" + fi + + if [ -z "$apar_rule_source" ]; then + log_fail "Repository-owned AudioReach udev rule was not found" + log_fail "Expected rule: config/udev/$apar_rule_name" + return 1 + fi + + if ! mkdir -p "$apar_rule_dir"; then + log_fail "Failed to create runtime udev rule directory: $apar_rule_dir" + return 1 + fi + + rm -f "$apar_rule_tmp" + + if ! cp "$apar_rule_source" "$apar_rule_tmp"; then + log_fail "Failed to copy AudioReach udev rule to temporary path" + rm -f "$apar_rule_tmp" + return 1 + fi + + if ! chmod 0644 "$apar_rule_tmp"; then + log_fail "Failed to set permissions on temporary AudioReach udev rule" + rm -f "$apar_rule_tmp" + return 1 + fi + + if ! mv "$apar_rule_tmp" "$apar_rule_target"; then + log_fail "Failed to install runtime AudioReach udev rule" + rm -f "$apar_rule_tmp" + return 1 + fi + + log_pass "Installed runtime AudioReach udev rule: $apar_rule_target" + + log_info "Reloading udev rules" + + if ! audio_exec_with_timeout \ + 10s \ + udevadm control --reload-rules 2>&1; then + log_fail "Failed to reload udev rules" + return 1 + fi + + if [ -d /sys/class/dma_heap ]; then + log_info "Retriggering dma_heap devices" + + if ! audio_exec_with_timeout \ + 15s \ + udevadm trigger \ + --action=change \ + --subsystem-match=dma_heap 2>&1; then + log_fail "Failed to retrigger dma_heap devices" + return 1 + fi + else + log_info "dma_heap subsystem is not currently enumerated; rule remains installed" + fi + + log_info "Waiting for udev event processing to settle" + + if ! audio_exec_with_timeout \ + 30s \ + udevadm settle 2>&1; then + log_fail "udev did not settle after AudioReach rule activation" + return 1 + fi + + # The node may not exist yet when the driver has not been loaded. Runtime + # preparation performs the mandatory existence check later. + if [ ! -e /dev/dma_heap/system ]; then + log_info "/dev/dma_heap/system is not currently present; permission validation is deferred" + return 0 + fi + + apar_dma_mode="$( + stat -c '%a' /dev/dma_heap/system 2>/dev/null || + echo unknown + )" + + apar_dma_owner="$( + stat -c '%U:%G' /dev/dma_heap/system 2>/dev/null || + echo unknown + )" + + log_info "/dev/dma_heap/system mode=$apar_dma_mode owner=$apar_dma_owner" + + if [ "$apar_dma_owner" != "root:audio" ]; then + log_fail "/dev/dma_heap/system has incorrect ownership: $apar_dma_owner" + log_fail "Expected ownership: root:audio" + return 1 + fi + + if [ "$apar_dma_mode" != "660" ]; then + log_fail "/dev/dma_heap/system has incorrect mode: $apar_dma_mode" + log_fail "Expected mode: 660" + return 1 + fi + + log_pass "/dev/dma_heap/system permissions are ready for the audio group" + return 0 +} + +# Ensure packages needed by tests under Runner/suites/Multimedia/Audio. +# +# Reuses lib_pkg_provider.sh directly. No package-provider wrapper functions are +# added here. +# +# Args: +# $1 - 0 for native/base mode, 1 when --overlay was explicitly requested +# +# Platform policy: +# qcom-distro/Yocto - strict no-op +# Debian base - ensure the mapped audio-base package set +# Debian overlay - ensure audio-base and Debian AudioReach package sets +# other distros - no-op until their package mappings are verified +# +# Return: +# 0 - ready or not applicable +# 1 - package preparation failed +# 2 - AudioReach DKMS package changed; reboot required +audio_prepare_test_packages() { + atp_overlay_requested="${1:-0}" + + if [ "$#" -gt 0 ]; then + shift + fi + + AUDIO_OVERLAY_PACKAGES_CHANGED=0 + AUDIO_OVERLAY_REBOOT_REQUIRED=0 + + export AUDIO_OVERLAY_PACKAGES_CHANGED + export AUDIO_OVERLAY_REBOOT_REQUIRED + + case "$atp_overlay_requested" in + 0|1) + ;; + *) + log_fail "Invalid Audio overlay request value: $atp_overlay_requested" + return 1 + ;; + esac + + # Resolve the operating-system ID without making the package-provider + # library mandatory on native embedded images. + if command -v pkg_detect_os_id >/dev/null 2>&1; then + atp_os_id="$( + pkg_detect_os_id 2>/dev/null || + echo unknown + )" + else + atp_os_id="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" + fi + + [ -n "$atp_os_id" ] || atp_os_id="unknown" + + case "$atp_os_id" in + qcom-distro|poky|openembedded|oe) + log_info "Native image detected; Audio package preparation is not required" + return 0 + ;; + debian) + ;; + *) + log_info "Audio package preparation is not enabled for os=$atp_os_id" + return 0 + ;; + esac + + # Debian package preparation must run before the test is re-executed as the + # unprivileged Audio user. + if [ "$(id -u 2>/dev/null || echo 1)" -ne 0 ]; then + log_fail "Debian Audio package preparation must run as root" + return 1 + fi + + ########################################################################### + # Debian base mode + ########################################################################### + + if [ "$atp_overlay_requested" -eq 0 ]; then + if ! command -v pkg_ensure_required_package_set_present \ + >/dev/null 2>&1; then + log_fail "Required package-set helper is unavailable" + return 1 + fi + + if ! pkg_ensure_required_package_set_present audio-base; then + log_fail "Failed to ensure Debian base Audio package set" + return 1 + fi + + log_pass "Debian base Audio package set is ready" + return 0 + fi + + ########################################################################### + # Debian AudioReach overlay mode + ########################################################################### + + if ! command -v pkg_ensure_optional_package_set_present \ + >/dev/null 2>&1; then + log_fail "Optional package-set helper is unavailable" + return 1 + fi + + if ! command -v pkg_installed_package_version \ + >/dev/null 2>&1; then + log_fail "Installed package-version helper is unavailable" + return 1 + fi + + # Capture AudioReach package versions before package preparation. These + # values let us distinguish an already-ready target from an install or + # upgrade performed during this invocation. + atp_before_plugin="$( + pkg_installed_package_version \ + audioreach-pipewire-plugin 2>/dev/null || + true + )" + + atp_before_dkms="$( + pkg_installed_package_version \ + audioreach-kernel-dkms 2>/dev/null || + true + )" + + atp_before_config="$( + pkg_installed_package_version \ + audioreach-config 2>/dev/null || + true + )" + + # The optional package provider intentionally requires the literal + # --overlay argument. Passing only the internal value "1" makes the provider + # treat the request as base mode. + if ! pkg_ensure_optional_package_set_present \ + audio \ + qli-staging \ + auto \ + --overlay \ + "$@"; then + log_fail "Failed to ensure Debian AudioReach package set" + return 1 + fi + + atp_after_plugin="$( + pkg_installed_package_version \ + audioreach-pipewire-plugin 2>/dev/null || + true + )" + + atp_after_dkms="$( + pkg_installed_package_version \ + audioreach-kernel-dkms 2>/dev/null || + true + )" + + atp_after_config="$( + pkg_installed_package_version \ + audioreach-config 2>/dev/null || + true + )" + + if [ "$atp_before_plugin" != "$atp_after_plugin" ] || + [ "$atp_before_dkms" != "$atp_after_dkms" ] || + [ "$atp_before_config" != "$atp_after_config" ]; then + AUDIO_OVERLAY_PACKAGES_CHANGED=1 + export AUDIO_OVERLAY_PACKAGES_CHANGED + + log_info "AudioReach package state changed during package preparation" + + if [ "$atp_before_plugin" != "$atp_after_plugin" ]; then + log_info "AudioReach PipeWire plugin version changed: ${atp_before_plugin:-not-installed} -> ${atp_after_plugin:-not-installed}" + fi + + if [ "$atp_before_config" != "$atp_after_config" ]; then + log_info "AudioReach configuration version changed: ${atp_before_config:-not-installed} -> ${atp_after_config:-not-installed}" + fi + + if [ "$atp_before_dkms" != "$atp_after_dkms" ]; then + log_info "AudioReach DKMS version changed: ${atp_before_dkms:-not-installed} -> ${atp_after_dkms:-not-installed}" + fi + else + log_info "AudioReach packages were already installed; runtime package state is unchanged" + fi + + # Do not continue into Audio testing after installing or upgrading the DKMS + # package. The active kernel must boot with the newly prepared module. + if [ "$atp_before_dkms" != "$atp_after_dkms" ]; then + AUDIO_OVERLAY_REBOOT_REQUIRED=1 + export AUDIO_OVERLAY_REBOOT_REQUIRED + + log_warn "AudioReach DKMS package changed" + log_warn "A reboot is required before running AudioReach validation" + return 2 + fi + + # Refresh module metadata when package state changed, then install and + # activate the repository-owned AudioReach udev rule. + if ! command -v audio_refresh_overlay_devices \ + >/dev/null 2>&1; then + log_fail "AudioReach device-refresh helper is unavailable" + return 1 + fi + + if ! audio_refresh_overlay_devices \ + "$atp_overlay_requested" \ + "$AUDIO_OVERLAY_PACKAGES_CHANGED"; then + log_fail "Failed to refresh AudioReach devices" + return 1 + fi + + log_pass "Debian AudioReach package set is ready" + return 0 +} + +# Prepare the Debian Audio test user and optional systemd user manager. +# +# Unlike audio_prepare_debian_audio_test_user(), this function does not: +# - re-execute the complete runner +# - change ownership of result or log directories +# - create or modify clip directories +# - invoke runuser +# +# The calling test remains the root orchestrator. Individual audio commands +# will be executed as the Debian Audio user through audio_run_as_test_user(). +# +# Args: +# $1 - 1 when a PipeWire systemd user manager is required +# 0 for ALSA-only operations +# +# Platform behavior: +# Debian: +# - require root orchestration +# - ensure AUDIO_TEST_USER belongs to audio +# - optionally start or refresh user@UID.service +# - export the resolved user-session environment +# +# Yocto/qcom-distro/other: +# - strict no-op +# +# Environment exported on Debian: +# AUDIO_TEST_USER +# AUDIO_TEST_UID +# AUDIO_TEST_HOME +# AUDIO_TEST_RUNTIME_DIR +# AUDIO_TEST_DBUS_ADDRESS +# AUDIO_SYSTEMCTL_USER_SCOPE +# +# Returns: +# 0 - preparation completed or not applicable +# 1 - preparation failed +audio_prepare_debian_audio_environment() { + apdae_need_user_manager="${1:-0}" + + case "$apdae_need_user_manager" in + 0|1) + ;; + *) + log_fail "Invalid Audio user-manager mode: $apdae_need_user_manager" + return 1 + ;; + esac + + ########################################################################### + # Platform detection + ########################################################################### + + if command -v pkg_detect_os_id >/dev/null 2>&1; then + apdae_os_id="$( + pkg_detect_os_id 2>/dev/null || + echo unknown + )" + else + apdae_os_id="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" + fi + + [ -n "$apdae_os_id" ] || apdae_os_id="unknown" + + case "$apdae_os_id" in + debian) + ;; + qcom-distro|poky|openembedded|oe) + return 0 + ;; + *) + return 0 + ;; + esac + + ########################################################################### + # Root orchestration + ########################################################################### + + if [ "$(id -u 2>/dev/null || echo 1)" -ne 0 ]; then + log_fail "Debian Audio environment preparation must run as root" + return 1 + fi + + apdae_user="${AUDIO_TEST_USER:-debian}" + + if ! id "$apdae_user" >/dev/null 2>&1; then + log_fail "Debian Audio test user does not exist: $apdae_user" + return 1 + fi + + if command -v getent >/dev/null 2>&1; then + if ! getent group audio >/dev/null 2>&1; then + log_fail "Required Debian group does not exist: audio" + return 1 + fi + elif ! grep -q '^audio:' /etc/group 2>/dev/null; then + log_fail "Required Debian group does not exist: audio" + return 1 + fi + + ########################################################################### + # Audio-group membership + ########################################################################### + + apdae_group_changed=0 + + if id -nG "$apdae_user" 2>/dev/null | + tr ' ' '\n' | + grep -qx audio; then + log_pass "User is already a member of audio: $apdae_user" + else + if ! command -v usermod >/dev/null 2>&1; then + log_fail "usermod is unavailable; cannot configure Audio test user" + return 1 + fi + + log_info "Adding Debian Audio test user to audio group: $apdae_user" + + if ! usermod -aG audio "$apdae_user"; then + log_fail "Failed to add $apdae_user to the audio group" + return 1 + fi + + apdae_group_changed=1 + + if ! id -nG "$apdae_user" 2>/dev/null | + tr ' ' '\n' | + grep -qx audio; then + log_fail "Audio-group membership was not applied to user: $apdae_user" + return 1 + fi + + log_pass "Added user to audio group: $apdae_user" + fi + + ########################################################################### + # User account details + ########################################################################### + + apdae_uid="$(id -u "$apdae_user" 2>/dev/null || true)" + + if [ -z "$apdae_uid" ]; then + log_fail "Unable to resolve uid for Audio test user: $apdae_user" + return 1 + fi + + if command -v getent >/dev/null 2>&1; then + apdae_passwd_entry="$( + getent passwd "$apdae_user" 2>/dev/null | + sed -n '1p' + )" + else + apdae_passwd_entry="$( + awk -F: -v requested_user="$apdae_user" \ + '$1 == requested_user { print; exit }' \ + /etc/passwd 2>/dev/null + )" + fi + + apdae_home="$( + printf '%s\n' "$apdae_passwd_entry" | + awk -F: 'NR == 1 { print $6 }' + )" + + [ -n "$apdae_home" ] || apdae_home="/home/$apdae_user" + + apdae_runtime_dir="/run/user/$apdae_uid" + apdae_bus_address="unix:path=$apdae_runtime_dir/bus" + + ########################################################################### + # Optional systemd user-manager preparation + ########################################################################### + + if [ "$apdae_need_user_manager" -eq 1 ]; then + if ! command -v systemctl >/dev/null 2>&1; then + log_fail "systemctl is unavailable; cannot prepare PipeWire user services" + return 1 + fi + + if [ ! -d /run/systemd/system ]; then + log_fail "systemd is not running; cannot prepare PipeWire user services" + return 1 + fi + + apdae_user_unit="user@${apdae_uid}.service" + + if systemctl is-active --quiet "$apdae_user_unit"; then + if [ "$apdae_group_changed" -eq 1 ]; then + log_info "Restarting Debian user manager after audio-group update: $apdae_user_unit" + + if command -v audio_exec_with_timeout >/dev/null 2>&1; then + audio_exec_with_timeout \ + 30s \ + systemctl restart "$apdae_user_unit" + apdae_systemctl_rc=$? + else + systemctl restart "$apdae_user_unit" + apdae_systemctl_rc=$? + fi + + if [ "$apdae_systemctl_rc" -ne 0 ]; then + log_fail "Failed to restart Debian user manager: $apdae_user_unit" + return 1 + fi + else + log_pass "Debian user manager is already active: $apdae_user_unit" + fi + else + log_info "Starting Debian user manager: $apdae_user_unit" + + systemctl reset-failed \ + "$apdae_user_unit" \ + >/dev/null 2>&1 || true + + if command -v audio_exec_with_timeout >/dev/null 2>&1; then + audio_exec_with_timeout \ + 30s \ + systemctl start "$apdae_user_unit" + apdae_systemctl_rc=$? + else + systemctl start "$apdae_user_unit" + apdae_systemctl_rc=$? + fi + + if [ "$apdae_systemctl_rc" -ne 0 ]; then + log_fail "Failed to start Debian user manager: $apdae_user_unit" + + systemctl status \ + "$apdae_user_unit" \ + --no-pager \ + --full 2>&1 | + while IFS= read -r apdae_line || + [ -n "$apdae_line" ]; do + log_info "[systemctl:user-manager] $apdae_line" + done + + return 1 + fi + fi + + apdae_wait=0 + apdae_wait_timeout="${AUDIO_USER_MANAGER_READY_TIMEOUT:-30}" + + case "$apdae_wait_timeout" in + ''|*[!0-9]*) + apdae_wait_timeout=30 + ;; + esac + + log_info "Waiting for Debian user runtime and D-Bus, timeout=${apdae_wait_timeout}s" + + while [ "$apdae_wait" -lt "$apdae_wait_timeout" ]; do + if [ -d "$apdae_runtime_dir" ] && + [ -S "$apdae_runtime_dir/bus" ]; then + break + fi + + sleep 1 + apdae_wait=$((apdae_wait + 1)) + done + + if [ ! -d "$apdae_runtime_dir" ]; then + log_fail "Debian user runtime directory is unavailable: $apdae_runtime_dir" + return 1 + fi + + if [ ! -S "$apdae_runtime_dir/bus" ]; then + log_fail "Debian user D-Bus is unavailable: $apdae_runtime_dir/bus" + return 1 + fi + + log_pass "Debian user runtime is ready: $apdae_runtime_dir" + fi + + ########################################################################### + # Export resolved environment for command-level user execution + ########################################################################### + + AUDIO_TEST_USER="$apdae_user" + AUDIO_TEST_UID="$apdae_uid" + AUDIO_TEST_HOME="$apdae_home" + AUDIO_TEST_RUNTIME_DIR="$apdae_runtime_dir" + AUDIO_TEST_DBUS_ADDRESS="$apdae_bus_address" + AUDIO_SYSTEMCTL_USER_SCOPE=1 + + export AUDIO_TEST_USER + export AUDIO_TEST_UID + export AUDIO_TEST_HOME + export AUDIO_TEST_RUNTIME_DIR + export AUDIO_TEST_DBUS_ADDRESS + export AUDIO_SYSTEMCTL_USER_SCOPE + + log_pass "Debian Audio environment prepared: user=$apdae_user uid=$apdae_uid" + return 0 +} + +# Execute one Audio command as the configured Debian Audio user. +# +# The main test runner remains the root orchestrator. Only the supplied command +# is executed as the Debian user. +# +# Usage: +# audio_run_as_test_user command [args...] +# +# audio_run_as_test_user \ +# --require-session \ +# command [args...] +# +# Options: +# --require-session +# Require the prepared systemd user runtime and D-Bus socket. Use this +# for PipeWire and systemctl --user commands. +# +# It is not required for direct ALSA commands such as aplay and arecord. +# +# Platform behavior: +# Debian: +# - require the caller to be root +# - verify the configured Audio user and audio-group membership +# - execute only the supplied command through runuser +# +# Yocto/qcom-distro/other: +# - execute the command directly as the current user +# +# Prerequisite on Debian: +# audio_prepare_debian_audio_environment must run before this helper. +# +# Returns: +# The exact exit status of the supplied command. +# 1 when user/session preparation is invalid. +audio_run_as_test_user() { + aratu_require_session=0 + + case "${1:-}" in + --require-session) + aratu_require_session=1 + shift + ;; + esac + + if [ "$#" -eq 0 ]; then + log_fail "audio_run_as_test_user requires a command" + return 1 + fi + + ########################################################################### + # Platform detection + ########################################################################### + + if command -v pkg_detect_os_id >/dev/null 2>&1; then + aratu_os_id="$( + pkg_detect_os_id 2>/dev/null || + echo unknown + )" + else + aratu_os_id="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" + fi + + [ -n "$aratu_os_id" ] || aratu_os_id="unknown" + + case "$aratu_os_id" in + debian) + ;; + *) + # Preserve existing native/Yocto execution behavior. + "$@" + return $? + ;; + esac + + ########################################################################### + # Debian root orchestration validation + ########################################################################### + + if [ "$(id -u 2>/dev/null || echo 1)" -ne 0 ]; then + log_fail "Debian Audio command execution must be initiated by root" + return 1 + fi + + if ! command -v runuser >/dev/null 2>&1; then + log_fail "runuser is unavailable; cannot execute an Audio command as the Debian user" + return 1 + fi + + aratu_user="${AUDIO_TEST_USER:-debian}" + + if ! id "$aratu_user" >/dev/null 2>&1; then + log_fail "Debian Audio test user does not exist: $aratu_user" + return 1 + fi + + if ! id -nG "$aratu_user" 2>/dev/null | + tr ' ' '\n' | + grep -qx audio; then + log_fail "Debian Audio test user is not a member of audio: $aratu_user" + log_fail "Call audio_prepare_debian_audio_environment before running user commands" + return 1 + fi + + ########################################################################### + # Resolve user account details + ########################################################################### + + aratu_uid="$(id -u "$aratu_user" 2>/dev/null || true)" + + if [ -z "$aratu_uid" ]; then + log_fail "Unable to resolve uid for Debian Audio test user: $aratu_user" + return 1 + fi + + aratu_home="${AUDIO_TEST_HOME:-}" + aratu_shell="" + + if command -v getent >/dev/null 2>&1; then + aratu_passwd_entry="$( + getent passwd "$aratu_user" 2>/dev/null | + sed -n '1p' + )" + else + aratu_passwd_entry="$( + awk -F: -v requested_user="$aratu_user" \ + '$1 == requested_user { print; exit }' \ + /etc/passwd 2>/dev/null + )" + fi + + if [ -z "$aratu_home" ]; then + aratu_home="$( + printf '%s\n' "$aratu_passwd_entry" | + awk -F: 'NR == 1 { print $6 }' + )" + fi + + aratu_shell="$( + printf '%s\n' "$aratu_passwd_entry" | + awk -F: 'NR == 1 { print $7 }' + )" + + [ -n "$aratu_home" ] || aratu_home="/home/$aratu_user" + [ -n "$aratu_shell" ] || aratu_shell="/bin/sh" + + aratu_runtime_dir="$( + printf '%s\n' \ + "${AUDIO_TEST_RUNTIME_DIR:-/run/user/$aratu_uid}" + )" + + aratu_bus_address="$( + printf '%s\n' \ + "${AUDIO_TEST_DBUS_ADDRESS:-unix:path=$aratu_runtime_dir/bus}" + )" + + ########################################################################### + # Optional user-session validation + ########################################################################### + + if [ "$aratu_require_session" -eq 1 ]; then + if [ ! -d "$aratu_runtime_dir" ]; then + log_fail "Debian Audio user runtime directory is unavailable: $aratu_runtime_dir" + return 1 + fi + + if [ ! -S "$aratu_runtime_dir/bus" ]; then + log_fail "Debian Audio user D-Bus socket is unavailable: $aratu_runtime_dir/bus" + return 1 + fi + fi + + ########################################################################### + # Execute only the requested command as the Debian Audio user + ########################################################################### + + case "${VERBOSE:-0}" in + 1) + log_info "Running Audio command as user=$aratu_user: $1" + ;; + esac + + runuser \ + -u "$aratu_user" \ + -- \ + env \ + HOME="$aratu_home" \ + USER="$aratu_user" \ + LOGNAME="$aratu_user" \ + SHELL="$aratu_shell" \ + TMPDIR="/tmp" \ + XDG_RUNTIME_DIR="$aratu_runtime_dir" \ + DBUS_SESSION_BUS_ADDRESS="$aratu_bus_address" \ + AUDIO_TEST_USER="$aratu_user" \ + AUDIO_TEST_UID="$aratu_uid" \ + AUDIO_TEST_HOME="$aratu_home" \ + AUDIO_TEST_RUNTIME_DIR="$aratu_runtime_dir" \ + AUDIO_TEST_DBUS_ADDRESS="$aratu_bus_address" \ + AUDIO_TEST_COMMAND_USER_CONTEXT=1 \ + "$@" + + return $? +} + +# Execute one existing audio_common.sh helper as the Debian Audio user. +# +# This preserves helper/library reuse while keeping the main test runner as the +# root orchestrator. On native/Yocto systems, the already-sourced helper is +# called directly in the current shell. +# +# Usage: +# audio_run_helper_as_test_user helper [args...] +# audio_run_helper_as_test_user --require-session helper [args...] +# +# Returns: +# The exact helper exit status. +# 1 for invalid arguments or missing preparation. +# 127 when the requested helper is unavailable in the child shell. +audio_run_helper_as_test_user() { + arhatu_require_session=0 + + case "${1:-}" in + --require-session) + arhatu_require_session=1 + shift + ;; + esac + + if [ "$#" -eq 0 ]; then + log_fail "audio_run_helper_as_test_user requires a helper name" + return 1 + fi + + arhatu_helper="$1" + shift + + if command -v pkg_detect_os_id >/dev/null 2>&1; then + arhatu_os_id="$( + pkg_detect_os_id 2>/dev/null || + echo unknown + )" + else + arhatu_os_id="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" + fi + + [ -n "$arhatu_os_id" ] || arhatu_os_id="unknown" + + case "$arhatu_os_id" in + debian) + ;; + *) + "$arhatu_helper" "$@" + return $? + ;; + esac + + if ! command -v audio_run_as_test_user >/dev/null 2>&1; then + log_fail "Audio user-command helper is unavailable" + return 1 + fi + + if [ -z "${TOOLS:-}" ] || + [ ! -r "$TOOLS/functestlib.sh" ] || + [ ! -r "$TOOLS/audio_common.sh" ]; then + log_fail "Audio helper libraries are unavailable in TOOLS=${TOOLS:-}" + return 1 + fi + + if [ "$arhatu_require_session" -eq 1 ]; then + # Variables in this command script are intentionally expanded by the child shell. + # shellcheck disable=SC2016 + audio_run_as_test_user \ + --require-session \ + sh -c ' + helper=$1 + shift + + . "$TOOLS/functestlib.sh" + . "$TOOLS/audio_common.sh" + + if ! command -v "$helper" >/dev/null 2>&1; then + exit 127 + fi + + "$helper" "$@" + ' \ + sh \ + "$arhatu_helper" \ + "$@" + else + # Variables in this command script are intentionally expanded by the child shell. + # shellcheck disable=SC2016 + audio_run_as_test_user \ + sh -c ' + helper=$1 + shift + + . "$TOOLS/functestlib.sh" + . "$TOOLS/audio_common.sh" + + if ! command -v "$helper" >/dev/null 2>&1; then + exit 127 + fi + + "$helper" "$@" + ' \ + sh \ + "$arhatu_helper" \ + "$@" + fi + + return $? +} + +# Execute one command through audio_exec_with_timeout as the Debian Audio user. +# Root opens any surrounding redirection before this helper is called, so test +# logs and result files remain root-owned. +# +# Usage: +# audio_run_with_timeout_as_test_user TIMEOUT command [args...] +# audio_run_with_timeout_as_test_user --require-session TIMEOUT command [args...] +audio_run_with_timeout_as_test_user() { + arwtatu_require_session=0 + + case "${1:-}" in + --require-session) + arwtatu_require_session=1 + shift + ;; + esac + + if [ "$#" -lt 2 ]; then + log_fail "audio_run_with_timeout_as_test_user requires TIMEOUT and command" + return 1 + fi + + arwtatu_timeout="$1" + shift + + if [ "$arwtatu_require_session" -eq 1 ]; then + audio_run_helper_as_test_user \ + --require-session \ + audio_exec_with_timeout \ + "$arwtatu_timeout" \ + "$@" + else + audio_run_helper_as_test_user \ + audio_exec_with_timeout \ + "$arwtatu_timeout" \ + "$@" + fi + + return $? +} + +# AudioRecord root-orchestrator helpers +# +# Insert this block into Runner/utils/audio_common.sh after +# audio_run_with_timeout_as_test_user(). +# +# These helpers are intentionally shared because they implement backend recovery, +# Debian-user capture workspace handling, mixer collection, and ALSA profile +# propagation. They do not parse AudioRecord CLI options or emit final results. + +audio_record_restart_backend_best_effort() { + arbbe_backend="$1" + + if [ "${AUDIO_RECORD_DEBIAN_ROOT_MODE:-0}" -ne 1 ]; then + audio_restart_services_best_effort + return $? + fi + + case "$arbbe_backend" in + pipewire) + audio_restart_pipewire_service "1/1" + ;; + pulseaudio) + audio_run_as_test_user \ + --require-session \ + systemctl --user restart pulseaudio + ;; + *) + return 1 + ;; + esac +} + +# On Debian, manual daemon bootstrap is replaced by one user-service recovery +# attempt. Native and minimal Yocto images retain their existing bootstrap path. +audio_record_bootstrap_backend_if_needed() { + if [ "${AUDIO_RECORD_DEBIAN_ROOT_MODE:-0}" -ne 1 ]; then + audio_bootstrap_backend_if_needed + return $? + fi + + case "${AUDIO_BACKEND:-pipewire}" in + pipewire|'') + audio_restart_pipewire_service "1/1" + ;; + pulseaudio) + audio_run_as_test_user \ + --require-session \ + systemctl --user restart pulseaudio + ;; + *) + return 1 + ;; + esac +} + +# Record whether a recovered backend is managed by the Debian user manager or +# by the native/minimal-image bootstrap path. +audio_record_set_recovered_backend_management() { + if [ "${AUDIO_RECORD_DEBIAN_ROOT_MODE:-0}" -eq 1 ]; then + AUDIO_SYSTEMD_MANAGED=1 + else + AUDIO_SYSTEMD_MANAGED=0 + fi + + export AUDIO_SYSTEMD_MANAGED +} + +# Resolve the root-owned final WAV and the Debian-user scratch WAV for one case. +audio_record_set_capture_paths() { + arcsp_case_name="$1" + + record_out="$LOGDIR/${arcsp_case_name}.wav" + + if [ "${AUDIO_RECORD_DEBIAN_ROOT_MODE:-0}" -eq 1 ]; then + record_user_out="$AUDIO_RECORD_USER_CAPTURE_DIR/${arcsp_case_name}.wav" + else + record_user_out="$record_out" + fi +} + +# Remove the previous scratch WAV without pre-creating a root-owned file. +audio_record_reset_capture_output() { + rm -f "$record_user_out" +} + +# Return the current scratch WAV size. +audio_record_capture_size() { + file_size_bytes "$record_user_out" 2>/dev/null || echo 0 +} + +# Move the Debian-user scratch WAV into the root-owned final results directory. +# When no scratch file was produced, create an empty final file so the existing +# WAV validator can report the backend failure consistently. +audio_record_promote_capture_output() { + if [ "$record_user_out" = "$record_out" ]; then + return 0 + fi + + rm -f "$record_out" + + if [ -f "$record_user_out" ]; then + if ! mv "$record_user_out" "$record_out"; then + log_fail "Failed to promote captured WAV into final results: $record_out" + return 1 + fi + + if ! chown 0:0 "$record_out"; then + log_fail "Failed to restore root ownership on captured WAV: $record_out" + return 1 + fi + + if ! chmod 0644 "$record_out"; then + log_fail "Failed to set captured WAV permissions: $record_out" + return 1 + fi + else + if ! : >"$record_out"; then + log_fail "Failed to initialize missing capture result: $record_out" + return 1 + fi + fi + + return 0 +} + +# Collect user-session mixer/control information while root owns the output file. +audio_record_dump_mixers() { + ardm_out="$1" + + if [ "${AUDIO_RECORD_DEBIAN_ROOT_MODE:-0}" -ne 1 ]; then + dump_mixers "$ardm_out" + return $? + fi + + if [ "${AUDIO_RECORD_USER_MANAGER_REQUIRED:-1}" -ne 1 ]; then + { + echo "---- wpctl status ----" + echo "(PipeWire user session was not requested for this ALSA-only run)" + echo "---- pactl list ----" + echo "(PulseAudio user session was not requested for this ALSA-only run)" + } >"$ardm_out" 2>/dev/null + return 0 + fi + + { + echo "---- wpctl status ----" + if command -v wpctl >/dev/null 2>&1; then + audio_run_with_timeout_as_test_user \ + --require-session \ + 2s \ + wpctl status 2>&1 || + echo "(wpctl status failed/timeout)" + else + echo "(wpctl not found)" + fi + + echo "---- pactl list ----" + if command -v pactl >/dev/null 2>&1; then + audio_run_with_timeout_as_test_user \ + --require-session \ + 3s \ + pactl list 2>&1 || + echo "(pactl list failed/timeout)" + else + echo "(pactl not found)" + fi + } >"$ardm_out" 2>/dev/null +} + +# Probe the ALSA capture path as the Debian Audio user while copying the +# discovered profile back into the root orchestrator. +audio_record_probe_alsa_capture_profile() { + if [ "${AUDIO_RECORD_DEBIAN_ROOT_MODE:-0}" -ne 1 ]; then + audio_probe_alsa_capture_profile + return $? + fi + + arpacp_output="$( + # Variables in this command script are intentionally expanded by the child shell. + # shellcheck disable=SC2016 + audio_run_as_test_user \ + sh -c ' + . "$TOOLS/functestlib.sh" + . "$TOOLS/audio_common.sh" + + audio_probe_alsa_capture_profile >/dev/null 2>&1 + probe_rc=$? + + printf "%s\n" \ + "${AUDIO_ALSA_CAPTURE_DEVICE:-}" \ + "${AUDIO_ALSA_CAPTURE_FORMAT:-}" \ + "${AUDIO_ALSA_CAPTURE_RATE:-}" \ + "${AUDIO_ALSA_CAPTURE_CHANNELS:-}" \ + "${AUDIO_ALSA_CAPTURE_REASON:-}" + + exit "$probe_rc" + ' + )" + arpacp_rc=$? + + AUDIO_ALSA_CAPTURE_DEVICE="$( + printf '%s\n' "$arpacp_output" | + sed -n '1p' + )" + AUDIO_ALSA_CAPTURE_FORMAT="$( + printf '%s\n' "$arpacp_output" | + sed -n '2p' + )" + AUDIO_ALSA_CAPTURE_RATE="$( + printf '%s\n' "$arpacp_output" | + sed -n '3p' + )" + AUDIO_ALSA_CAPTURE_CHANNELS="$( + printf '%s\n' "$arpacp_output" | + sed -n '4p' + )" + AUDIO_ALSA_CAPTURE_REASON="$( + printf '%s\n' "$arpacp_output" | + sed -n '5,$p' + )" + + export AUDIO_ALSA_CAPTURE_DEVICE + export AUDIO_ALSA_CAPTURE_FORMAT + export AUDIO_ALSA_CAPTURE_RATE + export AUDIO_ALSA_CAPTURE_CHANNELS + export AUDIO_ALSA_CAPTURE_REASON + + return "$arpacp_rc" +} + +# Prepare the Debian Audio test user and re-execute the current test as that +# user. +# +# Package preparation must complete successfully before calling this function. +# This helper does not install packages. +# +# Args: +# $1 - absolute path to the current run.sh +# $2 - result file path +# $3 - test-specific log/output directory +# $4 - 1 when the test requires PipeWire user services +# 0 for ALSA-only tests such as Audio_Card_Registration +# $5... - original run.sh arguments +# +# Platform behavior: +# Debian: +# - initial process must run as root +# - ensures AUDIO_TEST_USER belongs to the audio group +# - prepares test-owned result/output paths +# - starts or refreshes user@UID.service when requested +# - re-executes the runner as AUDIO_TEST_USER +# +# Yocto/qcom-distro/other: +# - strict no-op +# +# Environment: +# AUDIO_TEST_USER +# Debian test user; default: debian +# +# AUDIO_TEST_USER_REEXEC +# Internal recursion guard. +# +# AUDIO_PACKAGE_PREPARED +# Marks package preparation as already completed by root. +# +# AUDIO_SYSTEMCTL_USER_SCOPE +# Selects systemctl --user for PipeWire service operations. +# +# Returns: +# 0 - not applicable or already executing as the prepared Debian user +# 1 - user, output-path, or user-manager preparation failed +# +# A successful root-to-user transition uses exec and therefore does not return. +audio_prepare_debian_audio_test_user() { + if [ "$#" -lt 4 ]; then + log_fail "audio_prepare_debian_audio_test_user requires runner, result file, log directory, and user-manager mode" + return 1 + fi + + apdatu_script="$1" + apdatu_res_file="$2" + apdatu_log_dir="$3" + apdatu_need_user_manager="$4" + + shift 4 + + ########################################################################### + # Platform detection + ########################################################################### + + if command -v pkg_detect_os_id >/dev/null 2>&1; then + apdatu_os_id="$( + pkg_detect_os_id 2>/dev/null || + echo unknown + )" + else + apdatu_os_id="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" + fi + + [ -n "$apdatu_os_id" ] || apdatu_os_id="unknown" + + case "$apdatu_os_id" in + debian) + ;; + qcom-distro|poky|openembedded|oe) + # Preserve all native-image execution behavior. + return 0 + ;; + *) + # User switching is intentionally limited to verified Debian images. + return 0 + ;; + esac + + case "$apdatu_need_user_manager" in + 0|1) + ;; + *) + log_fail "Invalid Audio user-manager mode: $apdatu_need_user_manager" + return 1 + ;; + esac + + apdatu_user="${AUDIO_TEST_USER:-debian}" + apdatu_current_uid="$(id -u 2>/dev/null || echo 1)" + apdatu_current_user="$(id -un 2>/dev/null || echo unknown)" + + ########################################################################### + # Re-executed Debian child + ########################################################################### + + if [ "${AUDIO_TEST_USER_REEXEC:-0}" -eq 1 ]; then + if [ "$apdatu_current_user" != "$apdatu_user" ]; then + log_fail "Audio user re-exec mismatch: expected=$apdatu_user actual=$apdatu_current_user" + return 1 + fi + + if ! id -nG "$apdatu_user" 2>/dev/null | + tr ' ' '\n' | + grep -qx audio; then + log_fail "Debian Audio test user is not a member of audio: $apdatu_user" + return 1 + fi + + apdatu_uid="$(id -u "$apdatu_user" 2>/dev/null || true)" + + if [ -z "$apdatu_uid" ]; then + log_fail "Unable to resolve uid for Debian Audio test user: $apdatu_user" + return 1 + fi + + XDG_RUNTIME_DIR="/run/user/$apdatu_uid" + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$apdatu_uid/bus" + AUDIO_TEST_UID="$apdatu_uid" + AUDIO_SYSTEMCTL_USER_SCOPE=1 + + export XDG_RUNTIME_DIR + export DBUS_SESSION_BUS_ADDRESS + export AUDIO_TEST_UID + export AUDIO_SYSTEMCTL_USER_SCOPE + + if [ "$apdatu_need_user_manager" -eq 1 ]; then + if [ ! -d "$XDG_RUNTIME_DIR" ]; then + log_fail "Debian user runtime directory is missing: $XDG_RUNTIME_DIR" + return 1 + fi + + if [ ! -S "$XDG_RUNTIME_DIR/bus" ]; then + log_fail "Debian user D-Bus is unavailable: $XDG_RUNTIME_DIR/bus" + return 1 + fi + fi + + log_pass "Audio test is running as Debian user: user=$apdatu_user uid=$apdatu_uid" + return 0 + fi + + ########################################################################### + # Initial Debian process must be root + ########################################################################### + + if [ "$apdatu_current_uid" -ne 0 ]; then + log_fail "Debian Audio tests must initially run as root" + log_fail "Package preparation and Audio user setup require root privileges" + return 1 + fi + + if [ -z "$apdatu_script" ] || [ ! -f "$apdatu_script" ]; then + log_fail "Audio runner was not found: $apdatu_script" + return 1 + fi + + if [ ! -x "$apdatu_script" ]; then + log_fail "Audio runner is not executable: $apdatu_script" + return 1 + fi + + if ! id "$apdatu_user" >/dev/null 2>&1; then + log_fail "Debian Audio test user does not exist: $apdatu_user" + return 1 + fi + + if command -v getent >/dev/null 2>&1; then + if ! getent group audio >/dev/null 2>&1; then + log_fail "Required Debian group does not exist: audio" + return 1 + fi + elif ! grep -q '^audio:' /etc/group 2>/dev/null; then + log_fail "Required Debian group does not exist: audio" + return 1 + fi + + ########################################################################### + # Audio-group membership + ########################################################################### + + apdatu_group_changed=0 + + if id -nG "$apdatu_user" 2>/dev/null | + tr ' ' '\n' | + grep -qx audio; then + log_pass "User is already a member of audio: $apdatu_user" + else + if ! command -v usermod >/dev/null 2>&1; then + log_fail "usermod is unavailable; cannot configure Audio test user" + return 1 + fi + + log_info "Adding Debian Audio test user to audio group: $apdatu_user" + + if ! usermod -aG audio "$apdatu_user"; then + log_fail "Failed to add $apdatu_user to the audio group" + return 1 + fi + + apdatu_group_changed=1 + + if ! id -nG "$apdatu_user" 2>/dev/null | + tr ' ' '\n' | + grep -qx audio; then + log_fail "Audio-group membership was not applied to user: $apdatu_user" + return 1 + fi + + log_pass "Added user to audio group: $apdatu_user" + fi + + apdatu_uid="$(id -u "$apdatu_user" 2>/dev/null || true)" + + if [ -z "$apdatu_uid" ]; then + log_fail "Unable to resolve uid for Debian Audio test user: $apdatu_user" + return 1 + fi + + if command -v getent >/dev/null 2>&1; then + apdatu_passwd_entry="$( + getent passwd "$apdatu_user" 2>/dev/null | + sed -n '1p' + )" + else + apdatu_passwd_entry="$( + awk -F: -v requested_user="$apdatu_user" \ + '$1 == requested_user { print; exit }' \ + /etc/passwd 2>/dev/null + )" + fi + + apdatu_home="$( + printf '%s\n' "$apdatu_passwd_entry" | + awk -F: 'NR == 1 { print $6 }' + )" + + apdatu_shell="$( + printf '%s\n' "$apdatu_passwd_entry" | + awk -F: 'NR == 1 { print $7 }' + )" + + [ -n "$apdatu_home" ] || apdatu_home="/home/$apdatu_user" + [ -n "$apdatu_shell" ] || apdatu_shell="/bin/sh" + + ########################################################################### + # Prepare test-owned output paths + ########################################################################### + + if [ -n "$apdatu_res_file" ]; then + apdatu_res_parent="$(dirname "$apdatu_res_file")" + + if ! mkdir -p "$apdatu_res_parent"; then + log_fail "Failed to create result-file directory: $apdatu_res_parent" + return 1 + fi + + if ! : >"$apdatu_res_file"; then + log_fail "Failed to prepare result file: $apdatu_res_file" + return 1 + fi + + if ! chown "$apdatu_user:audio" "$apdatu_res_file"; then + log_fail "Failed to assign result file to $apdatu_user:audio" + return 1 + fi + + if ! chmod 0664 "$apdatu_res_file"; then + log_fail "Failed to set result-file permissions: $apdatu_res_file" + return 1 + fi + + log_pass "Prepared Audio result file for user=$apdatu_user: $apdatu_res_file" + fi + + if [ -n "$apdatu_log_dir" ]; then + case "$apdatu_log_dir" in + /) + log_fail "Refusing to use the filesystem root as an Audio log directory" + return 1 + ;; + esac + + if ! mkdir -p "$apdatu_log_dir"; then + log_fail "Failed to create Audio log directory: $apdatu_log_dir" + return 1 + fi + + apdatu_script_dir="$( + cd "$(dirname "$apdatu_script")" 2>/dev/null && + pwd + )" + + apdatu_log_dir_abs="$( + cd "$apdatu_log_dir" 2>/dev/null && + pwd + )" + + if [ -n "$apdatu_script_dir" ] && + [ "$apdatu_log_dir_abs" = "$apdatu_script_dir" ]; then + log_fail "Refusing to recursively change ownership of the runner directory" + log_fail "Use a dedicated test-specific LOGDIR" + return 1 + fi + + if ! chown -R "$apdatu_user:audio" "$apdatu_log_dir"; then + log_fail "Failed to assign Audio log directory to $apdatu_user:audio" + return 1 + fi + + if ! chmod u+rwx "$apdatu_log_dir"; then + log_fail "Failed to make Audio log directory writable by $apdatu_user" + return 1 + fi + + log_pass "Prepared Audio log directory for user=$apdatu_user: $apdatu_log_dir" + fi + + ########################################################################### + # PipeWire user-manager preparation + ########################################################################### + + if [ "$apdatu_need_user_manager" -eq 1 ]; then + if ! command -v systemctl >/dev/null 2>&1; then + log_fail "systemctl is unavailable; cannot prepare PipeWire user services" + return 1 + fi + + if [ ! -d /run/systemd/system ]; then + log_fail "systemd is not running; cannot prepare PipeWire user services" + return 1 + fi + + apdatu_user_unit="user@${apdatu_uid}.service" + + if systemctl is-active --quiet "$apdatu_user_unit"; then + if [ "$apdatu_group_changed" -eq 1 ]; then + # The existing user manager was started before the audio-group update. + # Restart it so PipeWire inherits the new supplementary group. + log_info "Restarting Debian user manager after audio-group update: $apdatu_user_unit" + + if command -v audio_exec_with_timeout >/dev/null 2>&1; then + audio_exec_with_timeout \ + 30s \ + systemctl restart "$apdatu_user_unit" + apdatu_systemctl_rc=$? + else + systemctl restart "$apdatu_user_unit" + apdatu_systemctl_rc=$? + fi + + if [ "$apdatu_systemctl_rc" -ne 0 ]; then + log_fail "Failed to restart Debian user manager: $apdatu_user_unit" + return 1 + fi + else + log_pass "Debian user manager is already active: $apdatu_user_unit" + fi + else + log_info "Starting Debian user manager: $apdatu_user_unit" + + # Clear a previous failed state before attempting a new start. + systemctl reset-failed "$apdatu_user_unit" 2>/dev/null || true + + if command -v audio_exec_with_timeout >/dev/null 2>&1; then + audio_exec_with_timeout \ + 30s \ + systemctl start "$apdatu_user_unit" + apdatu_systemctl_rc=$? + else + systemctl start "$apdatu_user_unit" + apdatu_systemctl_rc=$? + fi + + if [ "$apdatu_systemctl_rc" -ne 0 ]; then + log_fail "Failed to start Debian user manager: $apdatu_user_unit" + + systemctl status \ + "$apdatu_user_unit" \ + --no-pager \ + --full 2>&1 | + while IFS= read -r apdatu_line || + [ -n "$apdatu_line" ]; do + log_info "[systemctl:user-manager] $apdatu_line" + done + + return 1 + fi + fi + + apdatu_runtime_dir="/run/user/$apdatu_uid" + apdatu_bus_path="$apdatu_runtime_dir/bus" + apdatu_wait=0 + apdatu_wait_timeout="${AUDIO_USER_MANAGER_READY_TIMEOUT:-30}" + + case "$apdatu_wait_timeout" in + ''|*[!0-9]*) + apdatu_wait_timeout=30 + ;; + esac + + log_info "Waiting for Debian user runtime and D-Bus, timeout=${apdatu_wait_timeout}s" + + while [ "$apdatu_wait" -lt "$apdatu_wait_timeout" ]; do + if [ -d "$apdatu_runtime_dir" ] && + [ -S "$apdatu_bus_path" ]; then + break + fi + + sleep 1 + apdatu_wait=$((apdatu_wait + 1)) + done + + if [ ! -d "$apdatu_runtime_dir" ]; then + log_fail "Debian user runtime directory is unavailable: $apdatu_runtime_dir" + return 1 + fi + + if [ ! -S "$apdatu_bus_path" ]; then + log_fail "Debian user D-Bus is unavailable: $apdatu_bus_path" + return 1 + fi + + log_pass "Debian user runtime is ready: $apdatu_runtime_dir" + else + apdatu_runtime_dir="/run/user/$apdatu_uid" + apdatu_bus_path="$apdatu_runtime_dir/bus" + fi + + ########################################################################### + # Re-execute the test as the Debian Audio user + ########################################################################### + + if ! command -v runuser >/dev/null 2>&1; then + log_fail "runuser is unavailable; cannot execute Audio test as $apdatu_user" + return 1 + fi + + AUDIO_TEST_USER="$apdatu_user" + AUDIO_TEST_UID="$apdatu_uid" + AUDIO_TEST_USER_REEXEC=1 + AUDIO_PACKAGE_PREPARED=1 + AUDIO_SYSTEMCTL_USER_SCOPE=1 + AUDIO_TEST_USER_MANAGER_REQUIRED="$apdatu_need_user_manager" + + export AUDIO_TEST_USER + export AUDIO_TEST_UID + export AUDIO_TEST_USER_REEXEC + export AUDIO_PACKAGE_PREPARED + export AUDIO_SYSTEMCTL_USER_SCOPE + export AUDIO_TEST_USER_MANAGER_REQUIRED + + log_info "Re-executing Audio test as user=$apdatu_user uid=$apdatu_uid" + + # Replacing the privileged wrapper is intentional. This ensures signals and + # the final test exit status belong directly to the unprivileged test process. + # shellcheck disable=SC2093 + exec runuser \ + -u "$apdatu_user" \ + --preserve-environment \ + -- \ + env \ + HOME="$apdatu_home" \ + USER="$apdatu_user" \ + LOGNAME="$apdatu_user" \ + SHELL="$apdatu_shell" \ + TMPDIR="/tmp" \ + XDG_RUNTIME_DIR="$apdatu_runtime_dir" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=$apdatu_bus_path" \ + AUDIO_TEST_USER="$apdatu_user" \ + AUDIO_TEST_UID="$apdatu_uid" \ + AUDIO_TEST_USER_REEXEC=1 \ + AUDIO_PACKAGE_PREPARED=1 \ + AUDIO_SYSTEMCTL_USER_SCOPE=1 \ + AUDIO_TEST_USER_MANAGER_REQUIRED="$apdatu_need_user_manager" \ + AUDIO_OVERLAY_PACKAGES_CHANGED="${AUDIO_OVERLAY_PACKAGES_CHANGED:-0}" \ + AUDIO_OVERLAY_REBOOT_REQUIRED="${AUDIO_OVERLAY_REBOOT_REQUIRED:-0}" \ + "$apdatu_script" \ + "$@" +} + +# Minimal WAV fallback for images without Python. It validates RIFF/WAVE/fmt/data, +# requested rate/channels, bounded duration, and signed PCM sample activity. +# WAVE_FORMAT_EXTENSIBLE PCM is supported. Float WAV validation requires Python. +audio_validate_recorded_wav_od() { + avrwo_file="$1" + avrwo_source_kind="$2" + avrwo_rate="$3" + avrwo_channels="$4" + avrwo_expected_seconds="$5" + avrwo_analyze_bytes="${AUDIO_RECORD_ANALYZE_BYTES:-4194304}" + avrwo_min_active="${AUDIO_RECORD_MIN_ACTIVE_SAMPLES:-100}" + avrwo_min_distinct="${AUDIO_RECORD_MIN_DISTINCT_SAMPLES:-4}" + avrwo_threshold_lsb="${AUDIO_RECORD_SAMPLE_THRESHOLD_LSB:-${AUDIO_RECORD_SAMPLE_THRESHOLD:-8}}" + + command -v od >/dev/null 2>&1 || return 1 + command -v dd >/dev/null 2>&1 || return 1 + [ -s "$avrwo_file" ] || return 1 + + avrwo_meta="$({ + dd if="$avrwo_file" bs=1 count=1048576 2>/dev/null | + od -An -v -tu1 + } | awk ' + function u16(i) { + return b[i] + (b[i + 1] * 256) + } + function u32(i) { + return b[i] + (b[i + 1] * 256) + (b[i + 2] * 65536) + (b[i + 3] * 16777216) + } + function fourcc(i) { + return sprintf("%c%c%c%c", b[i], b[i + 1], b[i + 2], b[i + 3]) + } + { + for (i = 1; i <= NF; i++) { + b[++n] = $i + } + } + END { + if (n < 44 || (fourcc(1) != "RIFF" && fourcc(1) != "RF64") || fourcc(9) != "WAVE") { + exit 1 + } + + pos = 13 + fmt = 0 + channels = 0 + rate = 0 + byte_rate = 0 + align = 0 + bits = 0 + + while ((pos + 7) <= n) { + id = fourcc(pos) + size = u32(pos + 4) + + if (id == "fmt " && size >= 16 && (pos + 23) <= n) { + fmt = u16(pos + 8) + channels = u16(pos + 10) + rate = u32(pos + 12) + byte_rate = u32(pos + 16) + align = u16(pos + 20) + bits = u16(pos + 22) + + # WAVE_FORMAT_EXTENSIBLE: first two bytes of SubFormat GUID. + if (fmt == 65534 && size >= 40 && (pos + 33) <= n) { + fmt = u16(pos + 32) + } + } + + if (id == "data") { + # AWK arrays are one-based. pos+7 is the zero-based byte offset used + # by dd skip= for the first payload byte at array index pos+8. + printf "%d|%d|%d|%d|%d|%d|%d|%d\n", pos + 7, size, fmt, channels, rate, byte_rate, align, bits + exit 0 + } + + pos += 8 + size + (size % 2) + } + + exit 1 + } + ' 2>/dev/null)" || return 1 + + IFS='|' read -r \ + avrwo_offset \ + avrwo_declared \ + avrwo_format \ + avrwo_actual_channels \ + avrwo_actual_rate \ + avrwo_byte_rate \ + avrwo_block_align \ + avrwo_bits </dev/null || { + log_warn "od WAV fallback supports PCM only; format=$avrwo_format requires Python" + return 1 + } + + case "$avrwo_bits" in + 8|16|24|32) + ;; + *) + log_warn "od WAV fallback does not support PCM bit depth $avrwo_bits" + return 1 + ;; + esac + + [ "$avrwo_actual_rate" = "$avrwo_rate" ] || return 1 + [ "$avrwo_actual_channels" = "$avrwo_channels" ] || return 1 + [ "$avrwo_byte_rate" -gt 0 ] 2>/dev/null || return 1 + [ "$avrwo_block_align" -gt 0 ] 2>/dev/null || return 1 + [ $((avrwo_block_align % avrwo_actual_channels)) -eq 0 ] 2>/dev/null || return 1 + [ "$avrwo_byte_rate" -eq $((avrwo_actual_rate * avrwo_block_align)) ] 2>/dev/null || return 1 + avrwo_bytes_per_sample=$((avrwo_block_align / avrwo_actual_channels)) + [ "$avrwo_bits" -gt 0 ] 2>/dev/null || return 1 + [ "$avrwo_bits" -le $((avrwo_bytes_per_sample * 8)) ] 2>/dev/null || return 1 + + avrwo_file_bytes="$(file_size_bytes "$avrwo_file" 2>/dev/null || echo 0)" + avrwo_available=$((avrwo_file_bytes - avrwo_offset)) + [ "$avrwo_available" -gt 0 ] 2>/dev/null || return 1 + + if [ "$avrwo_declared" -gt 0 ] 2>/dev/null && + [ "$avrwo_declared" -lt "$avrwo_available" ] 2>/dev/null; then + avrwo_usable="$avrwo_declared" + else + avrwo_usable="$avrwo_available" + fi + + # Analyze complete frames only. + avrwo_usable=$((avrwo_usable - (avrwo_usable % avrwo_block_align))) + [ "$avrwo_usable" -gt 0 ] 2>/dev/null || return 1 + + if [ "$avrwo_expected_seconds" -gt 0 ] 2>/dev/null; then + avrwo_duration_ok="$(awk \ + -v bytes="$avrwo_usable" \ + -v byte_rate="$avrwo_byte_rate" \ + -v expected="$avrwo_expected_seconds" \ + -v ratio="${AUDIO_RECORD_MIN_DURATION_RATIO:-0.70}" \ + 'BEGIN { actual=bytes/byte_rate; minimum=expected*ratio; if (minimum < 0.25) minimum=0.25; print (actual >= minimum) ? 1 : 0 }')" + [ "$avrwo_duration_ok" -eq 1 ] || return 1 + fi + + if [ "$avrwo_source_kind" = "null" ]; then + AUDIO_WAV_VALIDATION_SUMMARY="AUDIO_WAV_VALIDATION status=PASS reason=valid-null-source-recording validator=od payload_bytes=$avrwo_usable" + export AUDIO_WAV_VALIDATION_SUMMARY + return 0 + fi + + if [ "$avrwo_usable" -lt "$avrwo_analyze_bytes" ] 2>/dev/null; then + avrwo_count="$avrwo_usable" + else + avrwo_count="$avrwo_analyze_bytes" + fi + avrwo_count=$((avrwo_count - (avrwo_count % avrwo_block_align))) + [ "$avrwo_count" -gt 0 ] 2>/dev/null || return 1 + + avrwo_stats="$({ + dd if="$avrwo_file" \ + bs=1 \ + skip="$avrwo_offset" \ + count="$avrwo_count" \ + 2>/dev/null | + od -An -v -tu1 + } | awk \ + -v bits="$avrwo_bits" \ + -v min_active="$avrwo_min_active" \ + -v min_distinct="$avrwo_min_distinct" \ + -v threshold_lsb="$avrwo_threshold_lsb" ' + function abs_value(value) { + return value < 0 ? -value : value + } + function power2(exponent, value, i) { + value = 1 + for (i = 0; i < exponent; i++) value *= 2 + return value + } + function consume_sample(raw, sign_limit, full_range, threshold, key) { + sign_limit = power2(bits - 1) + full_range = power2(bits) + if (raw >= sign_limit) raw -= full_range + + samples++ + if (raw != 0) nonzero_samples++ + + threshold = (threshold_lsb / 32768.0) * sign_limit + if (threshold < 0) threshold = 0 + if (abs_value(raw) > threshold) active_samples++ + + if (distinct_samples < 256) { + key = sprintf("%.0f", raw) + if (!(key in seen)) { + seen[key] = 1 + distinct_samples++ + } + } + } + BEGIN { + bytes_per_sample = bits / 8 + } + { + for (i = 1; i <= NF; i++) { + sample_bytes[++sample_index] = $i + if (sample_index == bytes_per_sample) { + raw = 0 + multiplier = 1 + for (j = 1; j <= bytes_per_sample; j++) { + raw += sample_bytes[j] * multiplier + multiplier *= 256 + delete sample_bytes[j] + } + sample_index = 0 + consume_sample(raw) + } + } + } + END { + printf "%d|%d|%d|%d\n", samples, nonzero_samples, active_samples, distinct_samples + if (samples <= 0 || nonzero_samples <= 0 || active_samples < min_active || distinct_samples < min_distinct) { + exit 1 + } + } + ')" || return 1 + + IFS='|' read -r \ + avrwo_samples \ + avrwo_nonzero_samples \ + avrwo_active_samples \ + avrwo_distinct_samples </dev/null 2>&1 && [ -r "$avrw_validator" ]; then + avrw_output="$(python3 "$avrw_validator" \ + --file "$avrw_file" \ + --source-kind "$avrw_source_kind" \ + --expect-rate "$avrw_rate" \ + --expect-channels "$avrw_channels" \ + --expected-seconds "$avrw_expected_seconds" \ + --analyze-bytes "${AUDIO_RECORD_ANALYZE_BYTES:-4194304}" \ + --min-active-samples "${AUDIO_RECORD_MIN_ACTIVE_SAMPLES:-100}" \ + --sample-threshold-lsb "${AUDIO_RECORD_SAMPLE_THRESHOLD_LSB:-${AUDIO_RECORD_SAMPLE_THRESHOLD:-8}}" \ + --min-distinct-samples "${AUDIO_RECORD_MIN_DISTINCT_SAMPLES:-4}" \ + --min-duration-ratio "${AUDIO_RECORD_MIN_DURATION_RATIO:-0.70}" \ + --strict-signal "${AUDIO_RECORD_STRICT_SIGNAL:-0}" \ + --min-rms-dbfs "${AUDIO_RECORD_MIN_RMS_DBFS:--60}" \ + 2>&1)" + avrw_rc=$? + + [ -n "$avrw_log" ] && printf '%s\n' "$avrw_output" >>"$avrw_log" + printf '%s\n' "$avrw_output" | + while IFS= read -r avrw_line; do + [ -n "$avrw_line" ] && log_info "$avrw_line" + done + + AUDIO_WAV_VALIDATION_SUMMARY="$(printf '%s\n' "$avrw_output" | tail -n 1)" + export AUDIO_WAV_VALIDATION_SUMMARY + [ "$avrw_rc" -eq 0 ] + return $? + fi + + log_warn "Python WAV validator unavailable; using bounded od/dd fallback" + if audio_validate_recorded_wav_od \ + "$avrw_file" \ + "$avrw_source_kind" \ + "$avrw_rate" \ + "$avrw_channels" \ + "$avrw_expected_seconds"; then + [ -n "$avrw_log" ] && printf '%s\n' "$AUDIO_WAV_VALIDATION_SUMMARY" >>"$avrw_log" + log_info "$AUDIO_WAV_VALIDATION_SUMMARY" + return 0 + fi + + AUDIO_WAV_VALIDATION_SUMMARY="AUDIO_WAV_VALIDATION status=FAIL reason=od-fallback-validation-failed" + export AUDIO_WAV_VALIDATION_SUMMARY + [ -n "$avrw_log" ] && printf '%s\n' "$AUDIO_WAV_VALIDATION_SUMMARY" >>"$avrw_log" + log_fail "$AUDIO_WAV_VALIDATION_SUMMARY" + return 1 +} + +# Validate one recorder result using the existing timeout and file-size helpers +# plus the WAV payload validator above. +# +# Args: +# $1 file +# $2 source kind: mic|null +# $3 expected rate, 0 disables the rate check +# $4 expected channels, 0 disables the channel check +# $5 expected recording duration +# $6 recorder return code +# $7 watchdog timeout passed to audio_exec_with_timeout +# $8 optional log file +# +# Return: +# 0 - recorder status is acceptable and WAV validation passed +# 1 - recorder status or WAV validation failed +audio_validate_recording_result() { + avrr_file="$1" + avrr_source_kind="${2:-mic}" + avrr_rate="${3:-0}" + avrr_channels="${4:-0}" + avrr_expected_duration="${5:-0}" + avrr_rc="${6:-1}" + avrr_timeout="${7:-0}" + avrr_log="${8:-}" + + avrr_bytes="$(file_size_bytes "$avrr_file" 2>/dev/null || echo 0)" + if [ "${avrr_bytes:-0}" -le 44 ] 2>/dev/null; then + AUDIO_WAV_VALIDATION_SUMMARY="AUDIO_WAV_VALIDATION status=FAIL reason=file-empty-or-header-only file_bytes=${avrr_bytes:-0}" + export AUDIO_WAV_VALIDATION_SUMMARY + [ -n "$avrr_log" ] && printf '%s\n' "$AUDIO_WAV_VALIDATION_SUMMARY" >>"$avrr_log" + return 1 + fi + + avrr_expected_seconds="$(audio_parse_secs "$avrr_expected_duration" 2>/dev/null || echo 0)" + [ -n "$avrr_expected_seconds" ] || avrr_expected_seconds=0 + + if ! audio_validate_recorded_wav \ + "$avrr_file" \ + "$avrr_source_kind" \ + "$avrr_rate" \ + "$avrr_channels" \ + "$avrr_expected_seconds" \ + "$avrr_log"; then + return 1 + fi + + case "$avrr_rc" in + 0) + ;; + 124|137|143) + avrr_timeout_seconds="$(audio_parse_secs "$avrr_timeout" 2>/dev/null || echo 0)" + if [ -z "$avrr_timeout_seconds" ] || [ "$avrr_timeout_seconds" -le 0 ] 2>/dev/null; then + AUDIO_WAV_VALIDATION_SUMMARY="${AUDIO_WAV_VALIDATION_SUMMARY} recorder_rc=$avrr_rc recorder_status=unexpected-timeout" + export AUDIO_WAV_VALIDATION_SUMMARY + return 1 + fi + log_warn "Recorder ended through expected watchdog timeout rc=$avrr_rc; validated WAV payload is accepted" + ;; + *) + AUDIO_WAV_VALIDATION_SUMMARY="${AUDIO_WAV_VALIDATION_SUMMARY} recorder_rc=$avrr_rc recorder_status=failed" + export AUDIO_WAV_VALIDATION_SUMMARY + return 1 + ;; + esac + + if command -v sox >/dev/null 2>&1 && [ -n "$avrr_log" ]; then + { + printf '%s\n' "---- optional sox recording statistics ----" + sox "$avrr_file" -n stat 2>&1 || true + } >>"$avrr_log" + fi + + return 0 +} diff --git a/Runner/utils/audio_wav_validate.py b/Runner/utils/audio_wav_validate.py new file mode 100755 index 000000000..6ecb610d8 --- /dev/null +++ b/Runner/utils/audio_wav_validate.py @@ -0,0 +1,506 @@ +#!/usr/bin/env python3 +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +"""Validate WAV structure, duration, negotiated format, and signal activity. + +This parser intentionally does not trust RIFF/data sizes. Streaming recorders can +leave placeholder sizes when they are stopped by a timeout. Analysis is clamped +to bytes that actually exist in the file and is bounded/distributed for large +recordings. +""" + +from __future__ import annotations + +import argparse +import math +import os +import struct +import sys +from dataclasses import dataclass +from typing import BinaryIO, Iterable + + +class WavValidationError(Exception): + """Raised for malformed or unsupported WAV files.""" + + +@dataclass(frozen=True) +class WaveInfo: + container: str + format_code: int + format_name: str + channels: int + sample_rate: int + byte_rate: int + block_align: int + bits_per_sample: int + valid_bits_per_sample: int + data_offset: int + declared_data_bytes: int + available_data_bytes: int + usable_data_bytes: int + file_bytes: int + + @property + def frames(self) -> int: + return self.usable_data_bytes // self.block_align + + @property + def duration_seconds(self) -> float: + if self.sample_rate <= 0: + return 0.0 + return self.frames / float(self.sample_rate) + + +@dataclass +class SignalStats: + samples: int = 0 + active_samples: int = 0 + peak: float = 0.0 + sum_squares: float = 0.0 + distinct: set[object] | None = None + invalid_float_samples: int = 0 + + def __post_init__(self) -> None: + if self.distinct is None: + self.distinct = set() + + @property + def rms(self) -> float: + if self.samples == 0: + return 0.0 + return math.sqrt(self.sum_squares / self.samples) + + @property + def active_ratio(self) -> float: + if self.samples == 0: + return 0.0 + return self.active_samples / float(self.samples) + + +def _read_exact(handle: BinaryIO, count: int) -> bytes: + data = handle.read(count) + if len(data) != count: + raise WavValidationError( + f"unexpected-end-of-file: wanted={count} got={len(data)}" + ) + return data + + +def _format_name(code: int) -> str: + return { + 1: "pcm", + 3: "ieee-float", + }.get(code, f"format-{code}") + + +def parse_wave(path: str, max_header_bytes: int = 1024 * 1024) -> WaveInfo: + file_bytes = os.path.getsize(path) + if file_bytes < 44: + raise WavValidationError(f"file-too-small:{file_bytes}") + + with open(path, "rb") as handle: + header = _read_exact(handle, 12) + container = header[0:4].decode("ascii", errors="replace") + if container not in {"RIFF", "RF64"}: + raise WavValidationError(f"unsupported-container:{container}") + if header[8:12] != b"WAVE": + raise WavValidationError("missing-WAVE-signature") + + fmt_payload: bytes | None = None + data_offset: int | None = None + declared_data_bytes: int | None = None + + while handle.tell() + 8 <= file_bytes: + if handle.tell() > max_header_bytes: + raise WavValidationError( + f"data-chunk-not-found-within-{max_header_bytes}-bytes" + ) + + chunk_header = _read_exact(handle, 8) + chunk_id = chunk_header[0:4] + chunk_size = struct.unpack_from(" 0: + handle.seek(remaining, os.SEEK_CUR) + elif chunk_id == b"data": + data_offset = chunk_data_offset + declared_data_bytes = chunk_size + break + else: + next_offset = chunk_data_offset + chunk_size + if next_offset > file_bytes: + raise WavValidationError( + "truncated-chunk-before-data:" + f"id={chunk_id!r} declared={chunk_size}" + ) + handle.seek(chunk_size, os.SEEK_CUR) + + if chunk_size & 1: + if handle.tell() < file_bytes: + handle.seek(1, os.SEEK_CUR) + + if fmt_payload is None: + raise WavValidationError("missing-fmt-chunk") + if data_offset is None or declared_data_bytes is None: + raise WavValidationError("missing-data-chunk") + + if len(fmt_payload) < 16: + raise WavValidationError("truncated-fmt-chunk") + + format_code, channels, sample_rate, byte_rate, block_align, bits = ( + struct.unpack_from(" 64: + raise WavValidationError(f"invalid-channel-count:{channels}") + if sample_rate <= 0: + raise WavValidationError(f"invalid-sample-rate:{sample_rate}") + if block_align <= 0: + raise WavValidationError(f"invalid-block-align:{block_align}") + if block_align % channels != 0: + raise WavValidationError( + f"block-align-not-divisible-by-channels:{block_align}/{channels}" + ) + if byte_rate <= 0: + raise WavValidationError(f"invalid-byte-rate:{byte_rate}") + expected_byte_rate = sample_rate * block_align + if byte_rate != expected_byte_rate: + raise WavValidationError( + f"byte-rate-mismatch:{byte_rate}!={expected_byte_rate}" + ) + + bytes_per_sample = block_align // channels + container_bits = bytes_per_sample * 8 + if bits <= 0 or bits > container_bits: + raise WavValidationError( + f"invalid-bits-per-sample:{bits}>{container_bits}" + ) + if valid_bits <= 0 or valid_bits > container_bits: + raise WavValidationError( + f"invalid-valid-bits-per-sample:{valid_bits}>{container_bits}" + ) + if format_code == 1 and bytes_per_sample not in {1, 2, 3, 4, 8}: + raise WavValidationError( + f"unsupported-pcm-container-bytes:{bytes_per_sample}" + ) + if format_code == 3 and bytes_per_sample not in {4, 8}: + raise WavValidationError( + f"unsupported-float-container-bytes:{bytes_per_sample}" + ) + + available_data_bytes = max(0, file_bytes - data_offset) + if container == "RF64" or declared_data_bytes in {0, 0xFFFFFFFF}: + usable_data_bytes = available_data_bytes + else: + usable_data_bytes = min(declared_data_bytes, available_data_bytes) + + usable_data_bytes -= usable_data_bytes % block_align + if usable_data_bytes <= 0: + raise WavValidationError("empty-audio-payload") + + return WaveInfo( + container=container, + format_code=format_code, + format_name=_format_name(format_code), + channels=channels, + sample_rate=sample_rate, + byte_rate=byte_rate, + block_align=block_align, + bits_per_sample=bits, + valid_bits_per_sample=valid_bits, + data_offset=data_offset, + declared_data_bytes=declared_data_bytes, + available_data_bytes=available_data_bytes, + usable_data_bytes=usable_data_bytes, + file_bytes=file_bytes, + ) + + +def _window_ranges(total_bytes: int, limit_bytes: int, align: int) -> list[tuple[int, int]]: + limit_bytes = max(align, limit_bytes - (limit_bytes % align)) + if total_bytes <= limit_bytes: + return [(0, total_bytes)] + + window_count = 8 + window_size = max(align, limit_bytes // window_count) + window_size -= window_size % align + if window_size <= 0: + window_size = align + + max_start = total_bytes - window_size + positions: list[int] = [] + for index in range(window_count): + raw = round(max_start * index / float(window_count - 1)) + aligned = int(raw) - (int(raw) % align) + if aligned not in positions: + positions.append(aligned) + + return [(position, window_size) for position in positions] + + +def _decode_pcm(sample: bytes, container_bytes: int, valid_bits: int) -> tuple[float, int]: + if container_bytes == 1: + raw = sample[0] - 128 + scale = 128.0 + else: + raw = int.from_bytes(sample, "little", signed=True) + effective_bits = valid_bits if 1 < valid_bits <= container_bytes * 8 else container_bytes * 8 + scale = float(1 << (effective_bits - 1)) + + # WAVE_FORMAT_EXTENSIBLE may store valid bits left-aligned in a larger + # container. Shift only when the low padding bits are consistently zero. + padding = container_bytes * 8 - effective_bits + if padding > 0 and raw % (1 << padding) == 0: + raw >>= padding + + normalized = max(-1.0, min(1.0, raw / scale)) + return normalized, raw + + +def _decode_float(sample: bytes, container_bytes: int) -> tuple[float, float]: + value = struct.unpack(" SignalStats: + stats = SignalStats() + container_bytes = info.block_align // info.channels + ranges = _window_ranges(info.usable_data_bytes, analyze_bytes, info.block_align) + + with open(path, "rb") as handle: + for relative_offset, length in ranges: + handle.seek(info.data_offset + relative_offset) + payload = handle.read(length) + payload = payload[: len(payload) - (len(payload) % info.block_align)] + + for frame_offset in range(0, len(payload), info.block_align): + frame = payload[frame_offset : frame_offset + info.block_align] + for channel in range(info.channels): + start = channel * container_bytes + sample = frame[start : start + container_bytes] + + if info.format_code == 1: + normalized, distinct_value = _decode_pcm( + sample, container_bytes, info.valid_bits_per_sample + ) + else: + normalized, distinct_value = _decode_float( + sample, container_bytes + ) + if not math.isfinite(normalized): + stats.invalid_float_samples += 1 + continue + normalized = max(-1.0, min(1.0, normalized)) + distinct_value = round(float(distinct_value), 10) + + magnitude = abs(normalized) + stats.samples += 1 + stats.sum_squares += normalized * normalized + if magnitude > stats.peak: + stats.peak = magnitude + if magnitude > threshold: + stats.active_samples += 1 + if len(stats.distinct) < 256: + stats.distinct.add(distinct_value) + + return stats + + +def _dbfs(value: float) -> float: + if value <= 0.0: + return float("-inf") + return 20.0 * math.log10(value) + + +def _clean(value: object) -> str: + text = str(value) + return text.replace(" ", "_").replace("\n", "_") + + +def emit(status: str, reason: str, info: WaveInfo | None, stats: SignalStats | None, warnings: Iterable[str]) -> None: + fields: list[tuple[str, object]] = [ + ("status", status), + ("reason", reason), + ] + + if info is not None: + fields.extend( + [ + ("container", info.container), + ("format", info.format_name), + ("channels", info.channels), + ("rate_hz", info.sample_rate), + ("bits", info.bits_per_sample), + ("block_align", info.block_align), + ("file_bytes", info.file_bytes), + ("declared_data_bytes", info.declared_data_bytes), + ("available_data_bytes", info.available_data_bytes), + ("usable_data_bytes", info.usable_data_bytes), + ("duration_s", f"{info.duration_seconds:.3f}"), + ] + ) + + if stats is not None: + fields.extend( + [ + ("analyzed_samples", stats.samples), + ("active_samples", stats.active_samples), + ("active_ratio", f"{stats.active_ratio:.8f}"), + ("distinct_samples", len(stats.distinct)), + ("peak_dbfs", f"{_dbfs(stats.peak):.2f}"), + ("rms_dbfs", f"{_dbfs(stats.rms):.2f}"), + ("invalid_float_samples", stats.invalid_float_samples), + ] + ) + + warning_list = list(warnings) + fields.append(("warnings", ",".join(warning_list) if warning_list else "none")) + + print( + "AUDIO_WAV_VALIDATION " + + " ".join(f"{key}={_clean(value)}" for key, value in fields) + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--file", required=True) + parser.add_argument("--source-kind", choices=("mic", "null"), default="mic") + parser.add_argument("--expect-rate", type=int, default=0) + parser.add_argument("--expect-channels", type=int, default=0) + parser.add_argument("--expected-seconds", type=float, default=0.0) + parser.add_argument("--analyze-bytes", type=int, default=4 * 1024 * 1024) + parser.add_argument("--min-active-samples", type=int, default=100) + parser.add_argument("--sample-threshold-lsb", type=float, default=8.0) + parser.add_argument("--min-distinct-samples", type=int, default=4) + parser.add_argument("--min-duration-ratio", type=float, default=0.70) + parser.add_argument("--strict-signal", choices=("0", "1"), default="0") + parser.add_argument("--min-rms-dbfs", type=float, default=-60.0) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + warnings: list[str] = [] + + try: + info = parse_wave(args.file) + except (OSError, WavValidationError) as error: + emit("ERROR", str(error), None, None, warnings) + return 2 + + if args.expect_rate > 0 and info.sample_rate != args.expect_rate: + emit( + "FAIL", + f"sample-rate-mismatch:{info.sample_rate}!={args.expect_rate}", + info, + None, + warnings, + ) + return 1 + + if args.expect_channels > 0 and info.channels != args.expect_channels: + emit( + "FAIL", + f"channel-mismatch:{info.channels}!={args.expect_channels}", + info, + None, + warnings, + ) + return 1 + + if info.declared_data_bytes == 0 and info.available_data_bytes > 0: + warnings.append("declared-data-zero-with-payload") + elif info.declared_data_bytes not in {0xFFFFFFFF, info.available_data_bytes}: + if info.declared_data_bytes > info.available_data_bytes: + warnings.append("declared-data-exceeds-file") + elif info.declared_data_bytes < info.available_data_bytes: + warnings.append("trailing-bytes-after-data") + + if args.expected_seconds > 0.0: + minimum_duration = max(0.25, args.expected_seconds * args.min_duration_ratio) + if info.duration_seconds < minimum_duration: + emit( + "FAIL", + f"duration-too-short:{info.duration_seconds:.3f}<{minimum_duration:.3f}", + info, + None, + warnings, + ) + return 1 + + threshold = max(0.0, args.sample_threshold_lsb / 32768.0) + try: + stats = analyze_signal( + args.file, + info, + max(info.block_align, args.analyze_bytes), + threshold, + ) + except (OSError, struct.error, ValueError) as error: + emit("ERROR", f"signal-analysis-failed:{error}", info, None, warnings) + return 2 + + if stats.samples <= 0: + emit("FAIL", "no-decodable-samples", info, stats, warnings) + return 1 + + if args.source_kind == "null": + emit("PASS", "valid-null-source-recording", info, stats, warnings) + return 0 + + if stats.peak == 0.0: + emit("FAIL", "digital-silence-all-zero", info, stats, warnings) + return 1 + + if len(stats.distinct) < max(2, args.min_distinct_samples): + emit("FAIL", "constant-or-near-constant-payload", info, stats, warnings) + return 1 + + if stats.active_samples < args.min_active_samples: + emit( + "FAIL", + f"insufficient-active-samples:{stats.active_samples}<{args.min_active_samples}", + info, + stats, + warnings, + ) + return 1 + + if args.strict_signal == "1" and _dbfs(stats.rms) < args.min_rms_dbfs: + emit( + "FAIL", + f"rms-below-strict-threshold:{_dbfs(stats.rms):.2f}<{args.min_rms_dbfs:.2f}", + info, + stats, + warnings, + ) + return 1 + + if _dbfs(stats.rms) < -60.0: + warnings.append("very-low-rms") + + emit("PASS", "signal-activity-present", info, stats, warnings) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) + From 57fa19bf0a532e0d9a282e0cbfefcf8581357677 Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Sat, 25 Jul 2026 05:58:54 +0530 Subject: [PATCH 2/4] audio: migrate playback to root orchestration Keep AudioPlayback orchestration, logging, results, and asset handling under root while executing only playback and user-session operations as the Debian audio user. - remove complete-runner re-execution through runuser - keep audio clip download and extraction root-owned - run PipeWire, PulseAudio, and ALSA playback as the Debian user - use the prepared Debian user session for control operations - validate AudioReach device access before overlay playback - retain backend fallback, timeout, and streaming evidence checks - avoid stdout log and AudioClips ownership failures - preserve native Yocto and qcom-distro execution Signed-off-by: Srikanth Muppandam --- .../Multimedia/Audio/AudioPlayback/run.sh | 422 +++++++++++++----- 1 file changed, 322 insertions(+), 100 deletions(-) diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh index 0f91871c0..09eaf806d 100755 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh @@ -31,8 +31,6 @@ if [ -z "${__INIT_ENV_LOADED:-}" ]; then __INIT_ENV_LOADED=1 fi -# shellcheck disable=SC1090 -. "$INIT_ENV" # shellcheck disable=SC1091 . "$TOOLS/functestlib.sh" # shellcheck disable=SC1091 @@ -40,6 +38,14 @@ fi # shellcheck disable=SC1091 . "$TOOLS/lib_video.sh" +# audio_prepare_test_packages() in audio_common.sh reuses these helpers. +# Source the provider only when it was not already loaded by a shared library. +if ! command -v pkg_detect_os_id >/dev/null 2>&1 && + [ -r "$TOOLS/lib_pkg_provider.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$TOOLS/lib_pkg_provider.sh" +fi + SYSTEMD_AVAILABLE=0 if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then SYSTEMD_AVAILABLE=1 @@ -48,23 +54,47 @@ fi TESTNAME="AudioPlayback" RESULT_TESTNAME="$TESTNAME" RES_SUFFIX="" # Optional suffix for unique result files (e.g., "Config1") -# RES_FILE will be set after parsing command-line arguments +# RES_FILE and LOGDIR are resolved by the early non-consuming pre-parser + +# Pre-parse the options required by privileged Audio preparation without +# consuming the original argument list. The normal parser below still receives +# the complete CLI unchanged. +AUDIO_OVERLAY_REQUESTED=0 +AUDIO_EARLY_HELP_REQUESTED=0 +AUDIO_EARLY_EXPECT="" -# Pre-parse --res-suffix and --lava-testcase-id for early failure handling -# This ensures unique result files and unique testcase IDs even if setup fails in parallel CI runs -prev_arg="" for arg in "$@"; do - case "$prev_arg" in - --res-suffix) + case "$AUDIO_EARLY_EXPECT" in + res-suffix) RES_SUFFIX="$arg" + AUDIO_EARLY_EXPECT="" + continue ;; - --lava-testcase-id) + lava-testcase-id) RESULT_TESTNAME="$arg" + AUDIO_EARLY_EXPECT="" + continue + ;; + esac + + case "$arg" in + --overlay) + AUDIO_OVERLAY_REQUESTED=1 + ;; + --res-suffix) + AUDIO_EARLY_EXPECT="res-suffix" + ;; + --lava-testcase-id) + AUDIO_EARLY_EXPECT="lava-testcase-id" + ;; + --help|-h) + AUDIO_EARLY_HELP_REQUESTED=1 ;; esac - prev_arg="$arg" done +export AUDIO_OVERLAY_REQUESTED + # ---- Assets ---- AUDIO_TAR_URL="${AUDIO_TAR_URL:-https://github.com/qualcomm-linux/qcom-linux-testkit/releases/download/AudioClips-v1.1/AudioClips.tar.gz}" export AUDIO_TAR_URL @@ -83,6 +113,11 @@ EXTRACT_AUDIO_ASSETS="${EXTRACT_AUDIO_ASSETS:-true}" ENABLE_NETWORK_DOWNLOAD="${ENABLE_NETWORK_DOWNLOAD:-false}" # Default: no network operations AUDIO_CLIPS_BASE_DIR="${AUDIO_CLIPS_BASE_DIR:-}" # Custom path for audio clips (CI use) +# Only the explicit --overlay option enables Debian AudioReach preparation. +# AUDIO_OVERLAY_REQUESTED was derived above without consuming the CLI. +AUDIO_PLAYBACK_VOLUME="${AUDIO_PLAYBACK_VOLUME:-1.0}" +export AUDIO_OVERLAY_REQUESTED AUDIO_PLAYBACK_VOLUME + AUDIO_BOOTSTRAP_MODE="${AUDIO_BOOTSTRAP_MODE:-auto}" AUDIO_RUNTIME_DIR="${AUDIO_RUNTIME_DIR:-}" MINIMAL_RAMDISK_MODE=0 @@ -109,10 +144,14 @@ SSID="" PASSWORD="" usage() { - cat </dev/null 2>&1; then + log_fail "$TESTNAME FAIL - required helper is unavailable: audio_prepare_test_packages" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + +audio_prepare_test_packages \ + "$AUDIO_OVERLAY_REQUESTED" +audio_prepare_rc=$? + +case "$audio_prepare_rc" in + 0) + ;; + 2) + log_skip "$TESTNAME SKIP - AudioReach kernel package changed; reboot required" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" + exit 0 + ;; + *) + log_fail "$TESTNAME FAIL - audio package preparation failed" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + ;; +esac + +# Root owns orchestration outputs and downloaded assets. +if ! mkdir -p "$LOGDIR"; then + log_fail "$TESTNAME FAIL - failed to create log directory: $LOGDIR" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + +if ! : >"$LOGDIR/summary.txt"; then + log_fail "$TESTNAME FAIL - failed to initialize summary file: $LOGDIR/summary.txt" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + while [ $# -gt 0 ]; do case "$1" in + --overlay) + AUDIO_OVERLAY_REQUESTED=1 + export AUDIO_OVERLAY_REQUESTED + shift + ;; --backend) AUDIO_BACKEND="$2" shift 2 @@ -261,42 +416,87 @@ while [ $# -gt 0 ]; do esac done -# Auto-enable network download if WiFi credentials provided -if [ -n "$SSID" ] && [ -n "$PASSWORD" ]; then - log_info "WiFi credentials provided, auto-enabling network download" - ENABLE_NETWORK_DOWNLOAD=true +# Prepare only the Debian user capabilities required by the selected mode. +# Explicit base ALSA playback needs group membership but no systemd user +# manager. Overlay and managed backends require the user session. +AUDIO_PLAYBACK_USER_MANAGER_REQUIRED=1 +if [ "$AUDIO_OVERLAY_REQUESTED" -eq 0 ] && + [ "$AUDIO_BACKEND" = "alsa" ]; then + AUDIO_PLAYBACK_USER_MANAGER_REQUIRED=0 fi -# Generate result file path with optional suffix (after parsing CLI args) -# Use absolute path anchored to SCRIPT_DIR for consistency -if [ -n "$RES_SUFFIX" ]; then - RES_FILE="$SCRIPT_DIR/${TESTNAME}_${RES_SUFFIX}.res" - log_info "Using unique result file: $RES_FILE" +if ! command -v audio_prepare_debian_audio_environment >/dev/null 2>&1; then + log_fail "$TESTNAME FAIL - required helper is unavailable: audio_prepare_debian_audio_environment" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + +if ! audio_prepare_debian_audio_environment \ + "$AUDIO_PLAYBACK_USER_MANAGER_REQUIRED"; then + log_fail "$TESTNAME FAIL - Debian Audio environment preparation failed" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + +AUDIO_PLAYBACK_DEBIAN_ROOT_MODE=0 +if command -v pkg_detect_os_id >/dev/null 2>&1; then + AUDIO_PLAYBACK_OS_ID="$(pkg_detect_os_id 2>/dev/null || echo unknown)" else - RES_FILE="$SCRIPT_DIR/${TESTNAME}.res" + AUDIO_PLAYBACK_OS_ID="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" fi -# Initialize LOGDIR after parsing CLI args (to apply RES_SUFFIX correctly) -# Use absolute paths for LOGDIR to work from any directory -# Apply suffix for unique log directories per invocation (matches RES_FILE behavior) -LOGDIR="$SCRIPT_DIR/results/${TESTNAME}" -if [ -n "$RES_SUFFIX" ]; then - LOGDIR="${LOGDIR}_${RES_SUFFIX}" - log_info "Using unique log directory: $LOGDIR" +if [ "$AUDIO_PLAYBACK_OS_ID" = "debian" ]; then + AUDIO_PLAYBACK_DEBIAN_ROOT_MODE=1 +fi + +export AUDIO_PLAYBACK_DEBIAN_ROOT_MODE + +# Auto-enable network download if WiFi credentials provided +if [ -n "$SSID" ] && [ -n "$PASSWORD" ]; then + log_info "WiFi credentials provided, auto-enabling network download" + ENABLE_NETWORK_DOWNLOAD=true fi -mkdir -p "$LOGDIR" -# Initialize summary file to prevent accumulation from previous test runs -: > "$LOGDIR/summary.txt" +# Debian overlay runtime preparation runs from the root orchestrator after +# normal CLI parsing. Only its device and PipeWire probes execute as debian. +if [ "$AUDIO_OVERLAY_REQUESTED" -eq 1 ]; then + if ! command -v audio_prepare_overlay_runtime >/dev/null 2>&1; then + log_fail "$TESTNAME FAIL - required helper is unavailable: audio_prepare_overlay_runtime" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi -# ------------- Mode Detection and Validation ------------- + audio_prepare_overlay_runtime "$AUDIO_OVERLAY_REQUESTED" + audio_runtime_rc=$? -if [ "$SYSTEMD_AVAILABLE" -eq 1 ]; then + case "$audio_runtime_rc" in + 0) + ;; + 2) + log_skip "$TESTNAME SKIP - AudioReach kernel package changed; reboot required" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" + exit 0 + ;; + *) + log_fail "$TESTNAME FAIL - AudioReach runtime preparation failed" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + ;; + esac +elif [ "$SYSTEMD_AVAILABLE" -eq 1 ] && + [ "$AUDIO_PLAYBACK_DEBIAN_ROOT_MODE" -ne 1 ]; then + # Preserve the existing native/Yocto validation path. Debian base mode uses + # command-level user probes during backend discovery instead. if ! setup_overlay_audio_environment; then - log_warn "Overlay audio environment setup failed; continuing with backend recovery flow" + log_warn "Existing overlay audio environment validation failed; continuing with backend recovery flow" fi else - log_info "systemd not available; skipping overlay audio environment setup (minimal ramdisk mode)" + log_info "systemd not available; skipping legacy overlay environment check" fi if [ "$SYSTEMD_AVAILABLE" -eq 0 ]; then @@ -394,7 +594,7 @@ if [ -n "$AUDIO_CLIPS_BASE_DIR" ]; then log_info "Using custom audio clips path: $AUDIO_CLIPS_BASE_DIR" fi -log_info "Args: backend=${AUDIO_BACKEND:-auto} sink=$SINK_CHOICE loops=$LOOPS timeout=$TIMEOUT formats='$FORMATS' durations='$DURATIONS' strict=$STRICT dmesg=$DMESG_SCAN extract=$EXTRACT_AUDIO_ASSETS network_download=$ENABLE_NETWORK_DOWNLOAD clips_path=${AUDIO_CLIPS_BASE_DIR:-default} bootstrap=$AUDIO_BOOTSTRAP_MODE runtime_dir=${AUDIO_RUNTIME_DIR:-auto}" +log_info "Args: backend=${AUDIO_BACKEND:-auto} sink=$SINK_CHOICE overlay=$AUDIO_OVERLAY_REQUESTED volume=$AUDIO_PLAYBACK_VOLUME loops=$LOOPS timeout=$TIMEOUT formats='$FORMATS' durations='$DURATIONS' strict=$STRICT dmesg=$DMESG_SCAN extract=$EXTRACT_AUDIO_ASSETS network_download=$ENABLE_NETWORK_DOWNLOAD clips_path=${AUDIO_CLIPS_BASE_DIR:-default} bootstrap=$AUDIO_BOOTSTRAP_MODE runtime_dir=${AUDIO_RUNTIME_DIR:-auto}" # --- Rootfs minimum size check (mirror video policy) --- if [ "$TOP_LEVEL_RUN" -eq 1 ]; then @@ -468,7 +668,7 @@ fi # Resolve backend if [ -z "$AUDIO_BACKEND" ]; then - AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "")" + AUDIO_BACKEND="$(audio_run_helper_as_test_user --require-session detect_audio_backend 2>/dev/null || echo "")" fi AUDIO_SYSTEMD_MANAGED=0 @@ -480,15 +680,15 @@ fi export AUDIO_SYSTEMD_MANAGED if [ -z "$AUDIO_BACKEND" ]; then - if audio_playback_alsa_probe; then + if audio_run_helper_as_test_user audio_playback_alsa_probe; then AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 export AUDIO_SYSTEMD_MANAGED log_info "Using backend: alsa (direct minimal-build fallback)" - elif audio_bootstrap_backend_if_needed; then - AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "")" + elif audio_playback_bootstrap_backend_if_needed; then + AUDIO_BACKEND="$(audio_run_helper_as_test_user --require-session detect_audio_backend 2>/dev/null || echo "")" if [ -z "$AUDIO_BACKEND" ]; then - if audio_playback_alsa_probe; then + if audio_run_helper_as_test_user audio_playback_alsa_probe; then AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 export AUDIO_SYSTEMD_MANAGED @@ -510,14 +710,14 @@ log_info "Using backend: $AUDIO_BACKEND" backend_ok=0 if [ "$AUDIO_BACKEND" = "alsa" ]; then - if audio_playback_alsa_probe; then + if audio_run_helper_as_test_user audio_playback_alsa_probe; then backend_ok=1 fi else - if audio_backend_ready "$AUDIO_BACKEND"; then + if audio_run_helper_as_test_user --require-session audio_backend_ready "$AUDIO_BACKEND"; then backend_ok=1 else - if check_audio_daemon "$AUDIO_BACKEND"; then + if audio_run_helper_as_test_user --require-session check_audio_daemon "$AUDIO_BACKEND"; then backend_ok=1 fi fi @@ -526,12 +726,12 @@ fi if [ "$backend_ok" -ne 1 ]; then if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting restart+retry once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true - if audio_backend_ready "$AUDIO_BACKEND"; then + audio_playback_restart_backend_best_effort "$AUDIO_BACKEND" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session audio_wait_audio_ready 20 "$AUDIO_BACKEND" >/dev/null 2>&1 || true + if audio_run_helper_as_test_user --require-session audio_backend_ready "$AUDIO_BACKEND"; then backend_ok=1 else - if check_audio_daemon "$AUDIO_BACKEND"; then + if audio_run_helper_as_test_user --require-session check_audio_daemon "$AUDIO_BACKEND"; then backend_ok=1 fi fi @@ -539,14 +739,22 @@ if [ "$backend_ok" -ne 1 ]; then fi if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then - log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting manual bootstrap" - if audio_bootstrap_backend_if_needed; then - AUDIO_SYSTEMD_MANAGED=0 + if [ "$AUDIO_PLAYBACK_DEBIAN_ROOT_MODE" -eq 1 ]; then + log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting user-service recovery" + else + log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting manual bootstrap" + fi + if audio_playback_bootstrap_backend_if_needed; then + if [ "$AUDIO_PLAYBACK_DEBIAN_ROOT_MODE" -eq 1 ]; then + AUDIO_SYSTEMD_MANAGED=1 + else + AUDIO_SYSTEMD_MANAGED=0 + fi export AUDIO_SYSTEMD_MANAGED - if audio_backend_ready "$AUDIO_BACKEND"; then + if audio_run_helper_as_test_user --require-session audio_backend_ready "$AUDIO_BACKEND"; then backend_ok=1 else - if check_audio_daemon "$AUDIO_BACKEND"; then + if audio_run_helper_as_test_user --require-session check_audio_daemon "$AUDIO_BACKEND"; then backend_ok=1 fi fi @@ -554,7 +762,7 @@ if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then fi if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then - if audio_playback_alsa_probe; then + if audio_run_helper_as_test_user audio_playback_alsa_probe; then log_warn "$TESTNAME: falling back to ALSA direct playback path" AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -573,7 +781,7 @@ fi case "$AUDIO_BACKEND" in pipewire) if ! check_dependencies pw-play; then - if audio_playback_alsa_probe && check_dependencies aplay; then + if audio_run_helper_as_test_user audio_playback_alsa_probe && check_dependencies aplay; then log_warn "$TESTNAME: PipeWire playback utility missing - falling back to ALSA" AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -587,7 +795,7 @@ case "$AUDIO_BACKEND" in ;; pulseaudio) if ! check_dependencies paplay; then - if audio_playback_alsa_probe && check_dependencies aplay; then + if audio_run_helper_as_test_user audio_playback_alsa_probe && check_dependencies aplay; then log_warn "$TESTNAME: PulseAudio playback utility missing - falling back to ALSA" AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -614,17 +822,17 @@ case "$AUDIO_BACKEND" in esac if [ "$AUDIO_BACKEND" = "pipewire" ]; then - if ! audio_pw_ctl_ok 2>/dev/null; then + if ! audio_run_helper_as_test_user --require-session audio_pw_ctl_ok 2>/dev/null; then if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then log_warn "$TESTNAME: wpctl not responsive - attempting restart+retry once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true + audio_playback_restart_backend_best_effort "$AUDIO_BACKEND" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session audio_wait_audio_ready 20 "$AUDIO_BACKEND" >/dev/null 2>&1 || true else log_warn "$TESTNAME: PipeWire control-plane not responsive - attempting ALSA fallback" fi - if ! audio_pw_ctl_ok 2>/dev/null; then - if audio_playback_alsa_probe && check_dependencies aplay; then + if ! audio_run_helper_as_test_user --require-session audio_pw_ctl_ok 2>/dev/null; then + if audio_run_helper_as_test_user audio_playback_alsa_probe && check_dependencies aplay; then log_warn "$TESTNAME: falling back to ALSA direct playback path" AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -637,17 +845,17 @@ if [ "$AUDIO_BACKEND" = "pipewire" ]; then fi fi elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then - if ! audio_pa_ctl_ok 2>/dev/null; then + if ! audio_run_helper_as_test_user --require-session audio_pa_ctl_ok 2>/dev/null; then if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then log_warn "$TESTNAME: pactl not responsive - attempting restart+retry once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true + audio_playback_restart_backend_best_effort "$AUDIO_BACKEND" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session audio_wait_audio_ready 20 "$AUDIO_BACKEND" >/dev/null 2>&1 || true else log_warn "$TESTNAME: PulseAudio control-plane not responsive - attempting ALSA fallback" fi - if ! audio_pa_ctl_ok 2>/dev/null; then - if audio_playback_alsa_probe && check_dependencies aplay; then + if ! audio_run_helper_as_test_user --require-session audio_pa_ctl_ok 2>/dev/null; then + if audio_run_helper_as_test_user audio_playback_alsa_probe && check_dependencies aplay; then log_warn "$TESTNAME: falling back to ALSA direct playback path" AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -665,16 +873,16 @@ fi SINK_ID="" case "$AUDIO_BACKEND:$SINK_CHOICE" in pipewire:null) - SINK_ID="$(pw_default_null)" + SINK_ID="$(audio_run_helper_as_test_user --require-session pw_default_null)" ;; pipewire:*) - SINK_ID="$(pw_default_speakers)" + SINK_ID="$(audio_run_helper_as_test_user --require-session pw_default_speakers)" ;; pulseaudio:null) - SINK_ID="$(pa_default_null)" + SINK_ID="$(audio_run_helper_as_test_user --require-session pa_default_null)" ;; pulseaudio:*) - SINK_ID="$(pa_default_speakers)" + SINK_ID="$(audio_run_helper_as_test_user --require-session pa_default_speakers)" ;; alsa:null) SINK_ID="null" @@ -696,18 +904,32 @@ if [ -z "$SINK_ID" ]; then fi if [ "$AUDIO_BACKEND" = "pipewire" ]; then - SINK_NAME="$(pw_sink_name_safe "$SINK_ID")" - wpctl set-default "$SINK_ID" >/dev/null 2>&1 || true + SINK_NAME="$(audio_run_helper_as_test_user --require-session pw_sink_name_safe "$SINK_ID")" + audio_run_helper_as_test_user --require-session pw_set_default_sink "$SINK_ID" >/dev/null 2>&1 || + log_warn "Could not set PipeWire default sink id=$SINK_ID" + + # Apply speaker volume to the discovered sink only. Null sinks remain + # untouched because volume/mute state is irrelevant to null playback. + if [ "$SINK_CHOICE" != "null" ]; then + audio_run_with_timeout_as_test_user --require-session 3s \ + wpctl set-mute "$SINK_ID" 0 >/dev/null 2>&1 || + log_warn "Could not unmute PipeWire sink id=$SINK_ID" + + audio_run_with_timeout_as_test_user --require-session 3s \ + wpctl set-volume "$SINK_ID" "$AUDIO_PLAYBACK_VOLUME" >/dev/null 2>&1 || + log_warn "Could not set PipeWire sink volume id=$SINK_ID volume=$AUDIO_PLAYBACK_VOLUME" + fi + if [ -z "$SINK_NAME" ]; then SINK_NAME="unknown" fi log_info "Routing to sink: id=$SINK_ID name='$SINK_NAME' choice=$SINK_CHOICE" elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then - SINK_NAME="$(pa_sink_name "$SINK_ID")" + SINK_NAME="$(audio_run_helper_as_test_user --require-session pa_sink_name "$SINK_ID")" if [ -z "$SINK_NAME" ]; then SINK_NAME="$SINK_ID" fi - pa_set_default_sink "$SINK_ID" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session pa_set_default_sink "$SINK_ID" >/dev/null 2>&1 || true log_info "Routing to sink: name='$SINK_NAME' choice=$SINK_CHOICE" else SINK_NAME="$SINK_ID" @@ -722,7 +944,7 @@ fi min_ok=0 if [ "$dur_s" -gt 0 ] 2>/dev/null; then - min_ok=$($dur_s - 1) + min_ok=$((dur_s - 1)) if [ "$min_ok" -lt 1 ]; then min_ok=1 fi @@ -839,15 +1061,15 @@ if [ "$USE_CLIP_DISCOVERY" = "true" ]; then if [ "$AUDIO_BACKEND" = "pipewire" ]; then log_info "[$case_name] exec: pw-play -v \"$clip_path\"" - audio_exec_with_timeout "$effective_timeout" pw-play -v "$clip_path" >>"$logf" 2>&1 + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" pw-play -v "$clip_path" >>"$logf" 2>&1 rc=$? elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then log_info "[$case_name] exec: paplay --device=\"$SINK_NAME\" \"$clip_path\"" - audio_exec_with_timeout "$effective_timeout" paplay --device="$SINK_NAME" "$clip_path" >>"$logf" 2>&1 + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" paplay --device="$SINK_NAME" "$clip_path" >>"$logf" 2>&1 rc=$? else log_info "[$case_name] exec: aplay -D \"$SINK_NAME\" \"$clip_path\"" - audio_exec_with_timeout "$effective_timeout" aplay -D "$SINK_NAME" "$clip_path" >>"$logf" 2>&1 + audio_run_with_timeout_as_test_user "$effective_timeout" aplay -D "$SINK_NAME" "$clip_path" >>"$logf" 2>&1 rc=$? fi @@ -858,8 +1080,8 @@ if [ "$USE_CLIP_DISCOVERY" = "true" ]; then fi # Evidence collection - pw_ev="$(audio_evidence_pw_streaming || echo 0)" - pa_ev="$(audio_evidence_pa_streaming || echo 0)" + pw_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pw_streaming || echo 0)" + pa_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pa_streaming || echo 0)" # Minimal PulseAudio fallback if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then @@ -870,7 +1092,7 @@ if [ "$USE_CLIP_DISCOVERY" = "true" ]; then alsa_ev="$(audio_evidence_alsa_running_any || echo 0)" asoc_ev="$(audio_evidence_asoc_path_on || echo 0)" - pwlog_ev="$(audio_evidence_pw_log_seen || echo 0)" + pwlog_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pw_log_seen || echo 0)" if [ "$AUDIO_BACKEND" = "pulseaudio" ] || [ "$AUDIO_BACKEND" = "alsa" ]; then pwlog_ev=0 fi @@ -894,13 +1116,13 @@ if [ "$USE_CLIP_DISCOVERY" = "true" ]; then # Determine result (use clip-specific timeout thresholds) if [ "$rc" -eq 0 ]; then log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s)" - ok_runs=$((ok_runs + 1)) + ok_runs=$((ok_runs + 1)) elif [ "$rc" -eq 124 ] && [ "$clip_dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$clip_min_ok" ]; then log_warn "[$case_name] TIMEOUT ($TIMEOUT) - PASS (ran ~${last_elapsed}s, expected ${clip_duration}s)" - ok_runs=$((ok_runs + 1)) + ok_runs=$((ok_runs + 1)) elif [ "$rc" -ne 0 ] && { [ "$pw_ev" -eq 1 ] || [ "$pa_ev" -eq 1 ] || [ "$alsa_ev" -eq 1 ] || [ "$asoc_ev" -eq 1 ]; }; then log_warn "[$case_name] nonzero rc=$rc but evidence indicates playback - PASS" - ok_runs=$((ok_runs + 1)) + ok_runs=$((ok_runs + 1)) else log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s) - see $logf" fi @@ -934,7 +1156,7 @@ else if [ -z "$clip" ]; then log_warn "[$case_name] No clip mapping for format=$fmt duration=$dur" echo "$case_name SKIP (no clip mapping)" >> "$LOGDIR/summary.txt" - skip=$((skip + 1)) + skip=$((skip + 1)) continue fi @@ -952,7 +1174,7 @@ else log_info "[$case_name] Hint: Run with --enable-network-download to download clips" fi echo "$case_name SKIP (clip unavailable)" >> "$LOGDIR/summary.txt" - skip=$((skip + 1)) + skip=$((skip + 1)) continue fi fi @@ -976,27 +1198,27 @@ else if [ "$AUDIO_BACKEND" = "pipewire" ]; then log_info "[$case_name] exec: pw-play -v \"$clip\"" - audio_exec_with_timeout "$TIMEOUT" pw-play -v "$clip" >>"$logf" 2>&1 + audio_run_with_timeout_as_test_user --require-session "$TIMEOUT" pw-play -v "$clip" >>"$logf" 2>&1 rc=$? elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then log_info "[$case_name] exec: paplay --device=\"$SINK_NAME\" \"$clip\"" - audio_exec_with_timeout "$TIMEOUT" paplay --device="$SINK_NAME" "$clip" >>"$logf" 2>&1 + audio_run_with_timeout_as_test_user --require-session "$TIMEOUT" paplay --device="$SINK_NAME" "$clip" >>"$logf" 2>&1 rc=$? else log_info "[$case_name] exec: aplay -D \"$SINK_NAME\" \"$clip\"" - audio_exec_with_timeout "$TIMEOUT" aplay -D "$SINK_NAME" "$clip" >>"$logf" 2>&1 + audio_run_with_timeout_as_test_user "$TIMEOUT" aplay -D "$SINK_NAME" "$clip" >>"$logf" 2>&1 rc=$? fi end_s="$(date +%s 2>/dev/null || echo 0)" - last_elapsed=$((end_s - start_s)) + last_elapsed=$((end_s - start_s)) if [ "$last_elapsed" -lt 0 ]; then last_elapsed=0 fi # Evidence - pw_ev="$(audio_evidence_pw_streaming || echo 0)" - pa_ev="$(audio_evidence_pa_streaming || echo 0)" + pw_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pw_streaming || echo 0)" + pa_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pa_streaming || echo 0)" # Minimal PulseAudio fallback so pa_streaming doesn't read as 0 after teardown if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then @@ -1007,7 +1229,7 @@ else alsa_ev="$(audio_evidence_alsa_running_any || echo 0)" asoc_ev="$(audio_evidence_asoc_path_on || echo 0)" - pwlog_ev="$(audio_evidence_pw_log_seen || echo 0)" + pwlog_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pw_log_seen || echo 0)" if [ "$AUDIO_BACKEND" = "pulseaudio" ] || [ "$AUDIO_BACKEND" = "alsa" ]; then pwlog_ev=0 fi @@ -1030,25 +1252,25 @@ else if [ "$rc" -eq 0 ]; then log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s)" - ok_runs=$((ok_runs + 1)) + ok_runs=$((ok_runs + 1)) elif [ "$rc" -eq 124 ] && [ "$dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$min_ok" ]; then log_warn "[$case_name] TIMEOUT ($TIMEOUT) - PASS (ran ~${last_elapsed}s)" - ok_runs=$((ok_runs + 1)) + ok_runs=$((ok_runs + 1)) elif [ "$rc" -ne 0 ] && { [ "$pw_ev" -eq 1 ] || [ "$pa_ev" -eq 1 ] || [ "$alsa_ev" -eq 1 ] || [ "$asoc_ev" -eq 1 ]; }; then log_warn "[$case_name] nonzero rc=$rc but evidence indicates playback - PASS" - ok_runs=$((ok_runs + 1)) + ok_runs=$((ok_runs + 1)) else log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s) - see $logf" fi - i=$((i + 1)) + i=$((i + 1)) done if [ "$ok_runs" -ge 1 ]; then - pass=$((pass + 1)) + pass=$((pass + 1)) echo "$case_name PASS" >> "$LOGDIR/summary.txt" else - fail=$((fail + 1)) + fail=$((fail + 1)) echo "$case_name FAIL" >> "$LOGDIR/summary.txt" suite_rc=1 fi From ba47b85e54dcecb7b800253b231d5d1b7c6abe7d Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Sat, 25 Jul 2026 05:59:18 +0530 Subject: [PATCH 3/4] audio: migrate recording to root orchestration Keep AudioRecord orchestration and result validation under root while executing capture and user-session operations as the Debian audio user. - remove complete-runner re-execution through runuser - use a dedicated Debian-user capture workspace - run pw-record, parecord, and arecord as the Debian user - promote completed WAV files into the root-owned results directory - restore final capture ownership and permissions before validation - reuse shared backend recovery and capture workspace helpers - retain configuration discovery, fallback, timeout, and JUnit handling - fail recordings whose sample payload contains only digital zeros Signed-off-by: Srikanth Muppandam --- .../Multimedia/Audio/AudioRecord/run.sh | 739 ++++++++++++------ 1 file changed, 492 insertions(+), 247 deletions(-) diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh index 7196aba0e..6211f0531 100755 --- a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh @@ -33,6 +33,14 @@ fi # shellcheck disable=SC1091 . "$TOOLS/audio_common.sh" +# audio_prepare_test_packages() in audio_common.sh reuses these helpers. +# Source the provider only when it was not already loaded by a shared library. +if ! command -v pkg_detect_os_id >/dev/null 2>&1 && + [ -r "$TOOLS/lib_pkg_provider.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$TOOLS/lib_pkg_provider.sh" +fi + SYSTEMD_AVAILABLE=0 if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then SYSTEMD_AVAILABLE=1 @@ -41,23 +49,47 @@ fi TESTNAME="AudioRecord" RESULT_TESTNAME="$TESTNAME" RES_SUFFIX="" # Optional suffix for unique result files (e.g., "Config1") -# RES_FILE will be set after parsing command-line arguments +# RES_FILE and LOGDIR are resolved by the early non-consuming pre-parser + +# Pre-parse the options required by privileged Audio preparation without +# consuming the original argument list. The complete original "$@" must remain +# available for the root-to-debian re-exec performed below. +AUDIO_OVERLAY_REQUESTED=0 +AUDIO_EARLY_HELP_REQUESTED=0 +AUDIO_EARLY_EXPECT="" -# Pre-parse --res-suffix and --lava-testcase-id for early failure handling -# This ensures unique result files and unique testcase IDs even if setup fails in parallel CI runs -prev_arg="" for arg in "$@"; do - case "$prev_arg" in - --res-suffix) + case "$AUDIO_EARLY_EXPECT" in + res-suffix) RES_SUFFIX="$arg" + AUDIO_EARLY_EXPECT="" + continue ;; - --lava-testcase-id) + lava-testcase-id) RESULT_TESTNAME="$arg" + AUDIO_EARLY_EXPECT="" + continue + ;; + esac + + case "$arg" in + --overlay) + AUDIO_OVERLAY_REQUESTED=1 + ;; + --res-suffix) + AUDIO_EARLY_EXPECT="res-suffix" + ;; + --lava-testcase-id) + AUDIO_EARLY_EXPECT="lava-testcase-id" + ;; + --help|-h) + AUDIO_EARLY_HELP_REQUESTED=1 ;; esac - prev_arg="$arg" done +export AUDIO_OVERLAY_REQUESTED + # ---------------- Defaults / CLI ---------------- AUDIO_BACKEND="" SRC_CHOICE="${SRC_CHOICE:-mic}" # mic|null @@ -70,6 +102,17 @@ DMESG_SCAN="${DMESG_SCAN:-1}" VERBOSE=0 JUNIT_OUT="" +# Capture paths are assigned by audio_record_set_capture_paths() from +# audio_common.sh before every recording attempt. Declare them here so the +# runner owns the shared state explicitly and static analysis can track it. +record_out="" +record_user_out="" + +# Explicit AudioReach overlay selection and WAV signal policy. +# AUDIO_OVERLAY_REQUESTED was derived above without consuming the CLI. +AUDIO_RECORD_STRICT_SIGNAL="${AUDIO_RECORD_STRICT_SIGNAL:-auto}" +export AUDIO_OVERLAY_REQUESTED AUDIO_RECORD_STRICT_SIGNAL + # Minimal ramdisk audio bootstrap options AUDIO_BOOTSTRAP_MODE="${AUDIO_BOOTSTRAP_MODE:-auto}" # auto|true|false AUDIO_RUNTIME_DIR="${AUDIO_RUNTIME_DIR:-}" # optional override @@ -89,6 +132,8 @@ usage() { Usage: $0 [options] --backend {pipewire|pulseaudio|alsa} --source {mic|null} + --overlay Prepare the Debian Qualcomm AudioReach overlay + Without this flag, use the native/base stack. --config-name "record_config1" # Test specific config(s) by name (space-separated) # Also supports record_config1, record_config2, ..., record_config10 --config-filter "48KHz" # Filter configs by pattern @@ -124,8 +169,81 @@ Examples: EOF } +# Keep Debian service recovery inside the prepared user manager. Preserve the +# existing broad best-effort recovery only for native/Yocto execution. +# --help must not install packages, change user groups, create output files, or +# start a systemd user manager. +if [ "$AUDIO_EARLY_HELP_REQUESTED" -eq 1 ]; then + usage + exit 0 +fi + +# Resolve root-owned result and log paths before privileged preparation. +if [ -n "$RES_SUFFIX" ]; then + RES_FILE="$SCRIPT_DIR/${TESTNAME}_${RES_SUFFIX}.res" + LOGDIR="$SCRIPT_DIR/results/${TESTNAME}_${RES_SUFFIX}" + log_info "Using unique result file: $RES_FILE" + log_info "Using unique log directory: $LOGDIR" +else + RES_FILE="$SCRIPT_DIR/${TESTNAME}.res" + LOGDIR="$SCRIPT_DIR/results/${TESTNAME}" +fi + +# Package and udev preparation remain privileged. The complete runner stays +# the root orchestrator on Debian. +if ! command -v audio_prepare_test_packages >/dev/null 2>&1; then + log_fail "$TESTNAME FAIL - required helper is unavailable: audio_prepare_test_packages" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + +audio_prepare_test_packages \ + "$AUDIO_OVERLAY_REQUESTED" +audio_prepare_rc=$? + +case "$audio_prepare_rc" in + 0) + ;; + 2) + log_skip "$TESTNAME SKIP - AudioReach kernel package changed; reboot required" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" + exit 0 + ;; + *) + log_fail "$TESTNAME FAIL - audio package preparation failed" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + ;; +esac + +# Root owns the final result, logs, summaries, diagnostics, and WAV artifacts. +if ! mkdir -p "$LOGDIR"; then + log_fail "$TESTNAME FAIL - failed to create log directory: $LOGDIR" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + +# The Debian Audio user needs traverse-only access to reach its dedicated +# scratch capture directory. Final logs and WAV artifacts remain root-owned. +if ! chmod 0755 "$LOGDIR"; then + log_fail "$TESTNAME FAIL - failed to set log directory traversal mode: $LOGDIR" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + +if ! : >"$LOGDIR/summary.txt"; then + log_fail "$TESTNAME FAIL - failed to initialize summary file: $LOGDIR/summary.txt" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + while [ $# -gt 0 ]; do case "$1" in + --overlay) + AUDIO_OVERLAY_REQUESTED=1 + export AUDIO_OVERLAY_REQUESTED + shift + ;; --backend) AUDIO_BACKEND="$2" shift 2 @@ -214,36 +332,131 @@ while [ $# -gt 0 ]; do esac done -# Generate result file path with optional suffix (after parsing CLI args) -# Use absolute path anchored to SCRIPT_DIR for consistency -if [ -n "$RES_SUFFIX" ]; then - RES_FILE="$SCRIPT_DIR/${TESTNAME}_${RES_SUFFIX}.res" - log_info "Using unique result file: $RES_FILE" +case "$AUDIO_RECORD_STRICT_SIGNAL" in + auto) + AUDIO_RECORD_STRICT_SIGNAL="$STRICT" + ;; + 0|1) + ;; + *) + log_warn "Invalid AUDIO_RECORD_STRICT_SIGNAL='$AUDIO_RECORD_STRICT_SIGNAL'; using 0" + AUDIO_RECORD_STRICT_SIGNAL=0 + ;; +esac +export AUDIO_RECORD_STRICT_SIGNAL + +# Prepare only the Debian user capabilities required by the selected mode. +# Explicit base ALSA capture needs group membership but no systemd user +# manager. Overlay and managed backends require the user session. +AUDIO_RECORD_USER_MANAGER_REQUIRED=1 +if [ "$AUDIO_OVERLAY_REQUESTED" -eq 0 ] && + [ "$AUDIO_BACKEND" = "alsa" ]; then + AUDIO_RECORD_USER_MANAGER_REQUIRED=0 +fi + +for audio_required_helper in \ + audio_prepare_debian_audio_environment \ + audio_run_as_test_user \ + audio_run_helper_as_test_user \ + audio_run_with_timeout_as_test_user +do + if ! command -v "$audio_required_helper" >/dev/null 2>&1; then + log_fail "$TESTNAME FAIL - required helper is unavailable: $audio_required_helper" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi +done + +if ! audio_prepare_debian_audio_environment \ + "$AUDIO_RECORD_USER_MANAGER_REQUIRED"; then + log_fail "$TESTNAME FAIL - Debian Audio environment preparation failed" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + +AUDIO_RECORD_DEBIAN_ROOT_MODE=0 +if command -v pkg_detect_os_id >/dev/null 2>&1; then + AUDIO_RECORD_OS_ID="$(pkg_detect_os_id 2>/dev/null || echo unknown)" else - RES_FILE="$SCRIPT_DIR/${TESTNAME}.res" + AUDIO_RECORD_OS_ID="$( + sed -n 's/^ID=//p' /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//;s/"$//' | + tr '[:upper:]' '[:lower:]' + )" fi -# Initialize LOGDIR after parsing CLI args (to apply RES_SUFFIX correctly) -# Use absolute paths for LOGDIR to work from any directory -# Apply suffix for unique log directories per invocation (matches RES_FILE behavior) -LOGDIR="$SCRIPT_DIR/results/${TESTNAME}" -if [ -n "$RES_SUFFIX" ]; then - LOGDIR="${LOGDIR}_${RES_SUFFIX}" - log_info "Using unique log directory: $LOGDIR" +if [ "$AUDIO_RECORD_OS_ID" = "debian" ]; then + AUDIO_RECORD_DEBIAN_ROOT_MODE=1 +fi + +export AUDIO_RECORD_DEBIAN_ROOT_MODE + +# Only this scratch directory is writable by the Debian Audio user. Root keeps +# ownership of the final LOGDIR, logs, summary, JUnit data, and promoted WAVs. +AUDIO_RECORD_USER_CAPTURE_DIR="$LOGDIR/.user-capture" +export AUDIO_RECORD_USER_CAPTURE_DIR + +if [ "$AUDIO_RECORD_DEBIAN_ROOT_MODE" -eq 1 ]; then + rm -rf "$AUDIO_RECORD_USER_CAPTURE_DIR" + + if ! mkdir -p "$AUDIO_RECORD_USER_CAPTURE_DIR"; then + log_fail "$TESTNAME FAIL - failed to create Audio user capture directory" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + if ! chown "${AUDIO_TEST_USER:-debian}:audio" \ + "$AUDIO_RECORD_USER_CAPTURE_DIR"; then + log_fail "$TESTNAME FAIL - failed to assign Audio user capture directory" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + if ! chmod 0770 "$AUDIO_RECORD_USER_CAPTURE_DIR"; then + log_fail "$TESTNAME FAIL - failed to set Audio user capture directory mode" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_pass "Prepared Debian Audio capture workspace: $AUDIO_RECORD_USER_CAPTURE_DIR" fi -mkdir -p "$LOGDIR" -# Initialize summary file to prevent accumulation from previous test runs -: > "$LOGDIR/summary.txt" +# Debian overlay runtime preparation runs from the root orchestrator after +# normal CLI parsing. Only its device and PipeWire probes execute as debian. +if [ "$AUDIO_OVERLAY_REQUESTED" -eq 1 ]; then + if ! command -v audio_prepare_overlay_runtime >/dev/null 2>&1; then + log_fail "$TESTNAME FAIL - required helper is unavailable: audio_prepare_overlay_runtime" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + audio_prepare_overlay_runtime "$AUDIO_OVERLAY_REQUESTED" + audio_runtime_rc=$? -# Overlay setup must happen after CLI parsing so --help can exit cleanly. -# Make it best-effort; later backend recovery logic will handle retry/bootstrap. -if [ "$SYSTEMD_AVAILABLE" -eq 1 ]; then + case "$audio_runtime_rc" in + 0) + ;; + 2) + log_skip "$TESTNAME SKIP - AudioReach kernel package changed; reboot required" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" + exit 0 + ;; + *) + log_fail "$TESTNAME FAIL - AudioReach runtime preparation failed" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + ;; + esac +elif [ "$SYSTEMD_AVAILABLE" -eq 1 ] && + [ "$AUDIO_RECORD_DEBIAN_ROOT_MODE" -ne 1 ]; then + # Preserve the existing native/Yocto validation path. Debian base mode uses + # command-level user probes during backend discovery instead. if ! setup_overlay_audio_environment; then - log_warn "Overlay audio environment setup failed; continuing with backend recovery flow" + log_warn "Existing overlay audio environment validation failed; continuing with backend recovery flow" fi else - log_info "systemd not available; skipping overlay audio environment setup (minimal ramdisk mode)" + log_info "systemd not available; skipping legacy overlay environment check" fi # Minimal ramdisk detection and cleanup trap (kills any manually bootstrapped daemons) @@ -303,11 +516,11 @@ if [ -n "$CONFIG_NAMES" ] && [ -n "$CONFIG_FILTER" ]; then CONFIG_FILTER="" fi -log_info "Args: backend=${AUDIO_BACKEND:-auto} source=$SRC_CHOICE loops=$LOOPS durations='$DURATIONS' record_seconds=$RECORD_SECONDS timeout=$TIMEOUT strict=$STRICT dmesg=$DMESG_SCAN bootstrap=$AUDIO_BOOTSTRAP_MODE runtime_dir=${AUDIO_RUNTIME_DIR:-auto}" +log_info "Args: backend=${AUDIO_BACKEND:-auto} source=$SRC_CHOICE overlay=$AUDIO_OVERLAY_REQUESTED loops=$LOOPS durations='$DURATIONS' record_seconds=$RECORD_SECONDS timeout=$TIMEOUT strict=$STRICT signal_strict=$AUDIO_RECORD_STRICT_SIGNAL dmesg=$DMESG_SCAN bootstrap=$AUDIO_BOOTSTRAP_MODE runtime_dir=${AUDIO_RUNTIME_DIR:-auto}" # Resolve backend (allow minimal-build ALSA capture fallback) if [ -z "$AUDIO_BACKEND" ]; then - AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "")" + AUDIO_BACKEND="$(audio_run_helper_as_test_user --require-session detect_audio_backend 2>/dev/null || echo "")" fi AUDIO_SYSTEMD_MANAGED=0 @@ -329,12 +542,11 @@ AUDIO_ALSA_CAPTURE_CHANNELS="" AUDIO_ALSA_CAPTURE_REASON="" if [ -z "$AUDIO_BACKEND" ]; then - if audio_bootstrap_backend_if_needed; then - AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "")" - AUDIO_SYSTEMD_MANAGED=0 - export AUDIO_SYSTEMD_MANAGED + if audio_record_bootstrap_backend_if_needed; then + AUDIO_BACKEND="$(audio_run_helper_as_test_user --require-session detect_audio_backend 2>/dev/null || echo "")" + audio_record_set_recovered_backend_management else - if audio_probe_alsa_capture_profile; then + if audio_record_probe_alsa_capture_profile; then ALSA_CAPTURE_PROBED=1 AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -355,16 +567,16 @@ if [ "$AUDIO_BACKEND" = "alsa" ]; then if [ "$ALSA_CAPTURE_PROBED" -eq 1 ]; then backend_ok=1 else - if audio_probe_alsa_capture_profile; then + if audio_record_probe_alsa_capture_profile; then ALSA_CAPTURE_PROBED=1 backend_ok=1 fi fi else - if audio_backend_ready "$AUDIO_BACKEND"; then + if audio_run_helper_as_test_user --require-session audio_backend_ready "$AUDIO_BACKEND"; then backend_ok=1 else - if check_audio_daemon "$AUDIO_BACKEND"; then + if audio_run_helper_as_test_user --require-session check_audio_daemon "$AUDIO_BACKEND"; then backend_ok=1 fi fi @@ -373,28 +585,27 @@ fi if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting restart+retry once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true - if audio_backend_ready "$AUDIO_BACKEND"; then + audio_record_restart_backend_best_effort "$AUDIO_BACKEND" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session audio_wait_audio_ready 20 "$AUDIO_BACKEND" >/dev/null 2>&1 || true + if audio_run_helper_as_test_user --require-session audio_backend_ready "$AUDIO_BACKEND"; then backend_ok=1 else - if check_audio_daemon "$AUDIO_BACKEND"; then + if audio_run_helper_as_test_user --require-session check_audio_daemon "$AUDIO_BACKEND"; then backend_ok=1 fi fi else log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting manual bootstrap" - if audio_bootstrap_backend_if_needed; then + if audio_record_bootstrap_backend_if_needed; then backend_ok=1 - AUDIO_SYSTEMD_MANAGED=0 - export AUDIO_SYSTEMD_MANAGED - AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "$AUDIO_BACKEND")" + audio_record_set_recovered_backend_management + AUDIO_BACKEND="$(audio_run_helper_as_test_user --require-session detect_audio_backend 2>/dev/null || echo "$AUDIO_BACKEND")" fi fi fi if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then - if audio_probe_alsa_capture_profile; then + if audio_record_probe_alsa_capture_profile; then ALSA_CAPTURE_PROBED=1 AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -446,36 +657,34 @@ esac # ----- Control-plane sanity (prevents wpctl/pactl hangs during source selection) ----- if [ "$AUDIO_BACKEND" = "pipewire" ]; then - if ! audio_pw_ctl_ok 2>/dev/null; then + if ! audio_run_helper_as_test_user --require-session audio_pw_ctl_ok 2>/dev/null; then if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then log_warn "$TESTNAME: wpctl not responsive - attempting restart+retry once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true + audio_record_restart_backend_best_effort "$AUDIO_BACKEND" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session audio_wait_audio_ready 20 "$AUDIO_BACKEND" >/dev/null 2>&1 || true else log_warn "$TESTNAME: wpctl not responsive - attempting manual bootstrap" - audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true - AUDIO_SYSTEMD_MANAGED=0 - export AUDIO_SYSTEMD_MANAGED + audio_record_bootstrap_backend_if_needed >/dev/null 2>&1 || true + audio_record_set_recovered_backend_management fi - if ! audio_pw_ctl_ok 2>/dev/null; then + if ! audio_run_helper_as_test_user --require-session audio_pw_ctl_ok 2>/dev/null; then log_skip "$TESTNAME SKIP - PipeWire control-plane not responsive" echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" exit 0 fi fi elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then - if ! audio_pa_ctl_ok 2>/dev/null; then + if ! audio_run_helper_as_test_user --require-session audio_pa_ctl_ok 2>/dev/null; then if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then log_warn "$TESTNAME: pactl not responsive - attempting restart+retry once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true + audio_record_restart_backend_best_effort "$AUDIO_BACKEND" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session audio_wait_audio_ready 20 "$AUDIO_BACKEND" >/dev/null 2>&1 || true else log_warn "$TESTNAME: pactl not responsive - attempting manual bootstrap" - audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true - AUDIO_SYSTEMD_MANAGED=0 - export AUDIO_SYSTEMD_MANAGED + audio_record_bootstrap_backend_if_needed >/dev/null 2>&1 || true + audio_record_set_recovered_backend_management fi - if ! audio_pa_ctl_ok 2>/dev/null; then + if ! audio_run_helper_as_test_user --require-session audio_pa_ctl_ok 2>/dev/null; then log_skip "$TESTNAME SKIP - PulseAudio control-plane not responsive" echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" exit 0 @@ -487,16 +696,16 @@ fi SRC_ID="" case "$AUDIO_BACKEND:$SRC_CHOICE" in pipewire:null) - SRC_ID="$(pw_default_null_source)" + SRC_ID="$(audio_run_helper_as_test_user --require-session pw_default_null_source)" ;; pipewire:*) - SRC_ID="$(pw_default_mic)" + SRC_ID="$(audio_run_helper_as_test_user --require-session pw_default_mic)" ;; pulseaudio:null) - SRC_ID="$(pa_default_null_source)" + SRC_ID="$(audio_run_helper_as_test_user --require-session pa_default_null_source)" ;; pulseaudio:*) - SRC_ID="$(pa_default_mic)" + SRC_ID="$(audio_run_helper_as_test_user --require-session pa_default_mic)" ;; alsa:null) SRC_ID="" @@ -505,7 +714,7 @@ case "$AUDIO_BACKEND:$SRC_CHOICE" in if [ "$ALSA_CAPTURE_PROBED" -eq 1 ] && [ -n "$AUDIO_ALSA_CAPTURE_DEVICE" ]; then SRC_ID="$AUDIO_ALSA_CAPTURE_DEVICE" else - SRC_ID="$(alsa_pick_capture)" + SRC_ID="$(audio_run_helper_as_test_user alsa_pick_capture)" fi ;; esac @@ -517,16 +726,16 @@ esac # after creating empty files. if [ -z "$SRC_ID" ] && [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" = "pipewire" ]; then log_warn "$TESTNAME: no concrete PipeWire mic source found; probing direct ALSA capture path" - - if audio_probe_alsa_capture_profile; then + + if audio_record_probe_alsa_capture_profile; then ALSA_CAPTURE_PROBED=1 AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 export AUDIO_SYSTEMD_MANAGED - + SRC_ID="$AUDIO_ALSA_CAPTURE_DEVICE" SRC_LABEL="$SRC_ID" - + log_warn "$TESTNAME: falling back to direct ALSA capture device: $SRC_ID" else log_skip "$TESTNAME SKIP - no real capture source available; PipeWire mic source missing and ALSA capture probe failed: ${AUDIO_ALSA_CAPTURE_REASON:-capture path unavailable}" @@ -534,14 +743,14 @@ if [ -z "$SRC_ID" ] && [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" = "pipewi exit 0 fi fi - + if [ -z "$SRC_ID" ] && [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" != "pipewire" ]; then for b in $BACKENDS_TO_TRY; do [ "$b" = "$AUDIO_BACKEND" ] && continue - + case "$b" in pipewire) - cand="$(pw_default_mic)" + cand="$(audio_run_helper_as_test_user --require-session pw_default_mic)" if [ -n "$cand" ]; then AUDIO_BACKEND="pipewire" SRC_ID="$cand" @@ -550,7 +759,7 @@ if [ -z "$SRC_ID" ] && [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" != "pipew fi ;; pulseaudio) - cand="$(pa_default_mic)" + cand="$(audio_run_helper_as_test_user --require-session pa_default_mic)" if [ -n "$cand" ]; then AUDIO_BACKEND="pulseaudio" SRC_ID="$cand" @@ -559,15 +768,15 @@ if [ -z "$SRC_ID" ] && [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" != "pipew fi ;; alsa) - if audio_probe_alsa_capture_profile; then + if audio_record_probe_alsa_capture_profile; then ALSA_CAPTURE_PROBED=1 AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 export AUDIO_SYSTEMD_MANAGED - + SRC_ID="$AUDIO_ALSA_CAPTURE_DEVICE" SRC_LABEL="$SRC_ID" - + log_info "Falling back to backend: alsa (device=$SRC_ID)" break fi @@ -575,7 +784,7 @@ if [ -z "$SRC_ID" ] && [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" != "pipew esac done fi - + if [ -z "$SRC_ID" ]; then log_skip "$TESTNAME SKIP - requested source '$SRC_CHOICE' not available on any backend (${BACKENDS_TO_TRY:-unknown})" echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" @@ -607,7 +816,9 @@ if [ "$AUDIO_BACKEND" = "alsa" ]; then hw:[0-9]*,[0-9]*|plughw:[0-9]*,[0-9]*) : ;; *) - cand="$(arecord -l 2>/dev/null | sed -n 's/^card[[:space:]]*\([0-9][0-9]*\).*device[[:space:]]*\([0-9][0-9]*\).*/hw:\1,\2/p' | head -n 1)" + cand="$(audio_run_as_test_user arecord -l 2>/dev/null | + sed -n 's/^card[[:space:]]*\([0-9][0-9]*\).*device[[:space:]]*\([0-9][0-9]*\).*/hw:\1,\2/p' | + head -n 1)" if [ -z "$cand" ]; then cand="$(sed -n 's/^\([0-9][0-9]*\)-\([0-9][0-9]*\):.*capture.*/hw:\1,\2/p' /proc/asound/pcm 2>/dev/null | head -n 1)" fi @@ -626,8 +837,8 @@ fi # ---- Routing log / defaults per backend ---- if [ "$AUDIO_BACKEND" = "pipewire" ]; then if [ -n "$SRC_ID" ]; then - SRC_LABEL="$(pw_source_label_safe "$SRC_ID")" - pw_set_default_source "$SRC_ID" >/dev/null 2>&1 || true + SRC_LABEL="$(audio_run_helper_as_test_user --require-session pw_source_label_safe "$SRC_ID")" + audio_run_helper_as_test_user --require-session pw_set_default_source "$SRC_ID" >/dev/null 2>&1 || true [ -z "$SRC_LABEL" ] && SRC_LABEL="unknown" log_info "Routing to source: id/name=$SRC_ID label='$SRC_LABEL' choice=$SRC_CHOICE" else @@ -635,8 +846,8 @@ if [ "$AUDIO_BACKEND" = "pipewire" ]; then log_info "Routing to source: id/name=default label='default' choice=$SRC_CHOICE" fi elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then - SRC_LABEL="$(pa_source_name "$SRC_ID" 2>/dev/null || echo "$SRC_ID")" - pa_set_default_source "$SRC_ID" >/dev/null 2>&1 || true + SRC_LABEL="$(audio_run_helper_as_test_user --require-session pa_source_name "$SRC_ID" 2>/dev/null || echo "$SRC_ID")" + audio_run_helper_as_test_user --require-session pa_set_default_source "$SRC_ID" >/dev/null 2>&1 || true log_info "Routing to source: name='$SRC_LABEL' choice=$SRC_CHOICE" else # ALSA SRC_LABEL="${SRC_ID:-default}" @@ -723,6 +934,47 @@ auto_secs_for() { esac } +# Validate the final capture selected by the existing backend/retry flow. +# Size remains useful only for deciding whether compatibility fallbacks should +# run; PASS requires a structurally valid WAV with acceptable sample content. +audio_record_validate_final_output() { + recording_valid=0 + validation_expected_duration="$secs" + validation_requested_seconds="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)" + validation_timeout_seconds="$(audio_parse_secs "$effective_timeout" 2>/dev/null || echo 0)" + + [ -n "$validation_requested_seconds" ] || validation_requested_seconds=0 + [ -n "$validation_timeout_seconds" ] || validation_timeout_seconds=0 + + if [ "$validation_timeout_seconds" -gt 0 ] 2>/dev/null && + { [ "$validation_requested_seconds" -le 0 ] 2>/dev/null || + [ "$validation_timeout_seconds" -lt "$validation_requested_seconds" ] 2>/dev/null; }; then + validation_expected_duration="$effective_timeout" + fi + + AUDIO_WAV_VALIDATION_SUMMARY="AUDIO_WAV_VALIDATION status=FAIL reason=not-run" + export AUDIO_WAV_VALIDATION_SUMMARY + + if audio_validate_recording_result \ + "$record_out" \ + "$SRC_CHOICE" \ + "${validation_rate:-0}" \ + "${validation_channels:-0}" \ + "$validation_expected_duration" \ + "$rc" \ + "$effective_timeout" \ + "$logf"; then + recording_valid=1 + + case "$rc" in + 124|137|143) + # Normalize an expected watchdog stop only after WAV validation passes. + rc=0 + ;; + esac + fi +} + # ------------- Test Execution (Matrix or Config Discovery) ------------- total=0 pass=0 @@ -819,56 +1071,56 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then log_info "[$case_name] loop $i/$LOOPS start=$iso rate=${rate}Hz channels=$channels backend=$AUDIO_BACKEND $loop_hdr" - record_out="$LOGDIR/${case_name}.wav" - : > "$record_out" + audio_record_set_capture_paths "$case_name" + audio_record_reset_capture_output start_s="$(date +%s 2>/dev/null || echo 0)" + validation_rate="$rate" + validation_channels="$channels" if [ "$AUDIO_BACKEND" = "pipewire" ]; then - log_info "[$case_name] exec: pw-record -v --rate=$rate --channels=$channels \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" "$record_out" >> "$logf" 2>&1 + log_info "[$case_name] exec: pw-record -v --rate=$rate --channels=$channels \"$record_user_out\"" + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" # If pw-record failed AND PipeWire control-plane is broken, restart/bootstrap and retry once - if [ "$rc" -ne 0 ] && ! audio_pw_ctl_ok 2>/dev/null; then + if [ "$rc" -ne 0 ] && + ! audio_run_helper_as_test_user --require-session audio_pw_ctl_ok 2>/dev/null; then if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - restarting and retrying once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true + audio_record_restart_backend_best_effort "$AUDIO_BACKEND" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session audio_wait_audio_ready 20 "$AUDIO_BACKEND" >/dev/null 2>&1 || true else log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - attempting bootstrap and retrying once" - audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true - AUDIO_SYSTEMD_MANAGED=0 - export AUDIO_SYSTEMD_MANAGED + audio_record_bootstrap_backend_if_needed >/dev/null 2>&1 || true + audio_record_set_recovered_backend_management fi - record_out="$LOGDIR/${case_name}.wav" - : > "$record_out" - log_info "[$case_name] retry: pw-record -v --rate=$rate --channels=$channels \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" "$record_out" >> "$logf" 2>&1 + audio_record_set_capture_paths "$case_name" + audio_record_reset_capture_output + log_info "[$case_name] retry: pw-record -v --rate=$rate --channels=$channels \"$record_user_out\"" + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" fi - # If we already got real audio, accept and skip fallbacks + # A non-trivial file skips compatibility fallbacks. Content and + # recorder status are validated after the backend flow completes. if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - if [ "$rc" -ne 0 ]; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 - fi + : else # Only if output is tiny/empty do we try a virtual PCM if command -v arecord >/dev/null 2>&1; then - pcm="$(alsa_pick_virtual_pcm || true)" + pcm="$(audio_run_helper_as_test_user alsa_pick_virtual_pcm || true)" if [ -n "$pcm" ]; then secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)" [ -z "$secs_int" ] && secs_int=0 - : > "$record_out" - log_info "[$case_name] fallback: arecord -D $pcm -f S16_LE -r $rate -c $channels -d $secs_int \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" \ - arecord -D "$pcm" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$record_out" >> "$logf" 2>&1 + audio_record_reset_capture_output + log_info "[$case_name] fallback: arecord -D $pcm -f S16_LE -r $rate -c $channels -d $secs_int \"$record_user_out\"" + audio_run_with_timeout_as_test_user "$effective_timeout" \ + arecord -D "$pcm" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" fi fi @@ -882,29 +1134,23 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then fi fi if [ "$retry_target" -eq 1 ] && [ -n "$SRC_ID" ]; then - : > "$record_out" - log_info "[$case_name] exec: pw-record -v --rate=$rate --channels=$channels --target \"$SRC_ID\" \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" --target "$SRC_ID" "$record_out" >> "$logf" 2>&1 + audio_record_reset_capture_output + log_info "[$case_name] exec: pw-record -v --rate=$rate --channels=$channels --target \"$SRC_ID\" \"$record_user_out\"" + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" --target "$SRC_ID" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" fi fi - # Optional safety: If nonzero rc but output is clearly valid, accept - if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 - fi - else if [ "$AUDIO_BACKEND" = "alsa" ]; then secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)" [ -z "$secs_int" ] && secs_int=0 - log_info "[$case_name] exec: arecord -D \"$SRC_ID\" -f S16_LE -r $rate -c $channels -d $secs_int \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" \ - arecord -D "$SRC_ID" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$record_out" >> "$logf" 2>&1 + log_info "[$case_name] exec: arecord -D \"$SRC_ID\" -f S16_LE -r $rate -c $channels -d $secs_int \"$record_user_out\"" + audio_run_with_timeout_as_test_user "$effective_timeout" \ + arecord -D "$SRC_ID" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" retry_alsa=0 if [ "$rc" -ne 0 ]; then @@ -922,12 +1168,12 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then alt_dev="$SRC_ID" fi - : > "$record_out" - log_info "[$case_name] retry: arecord -D \"$alt_dev\" -f S16_LE -r $rate -c $channels -d $secs_int \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" \ - arecord -D "$alt_dev" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$record_out" >> "$logf" 2>&1 + audio_record_reset_capture_output + log_info "[$case_name] retry: arecord -D \"$alt_dev\" -f S16_LE -r $rate -c $channels -d $secs_int \"$record_user_out\"" + audio_run_with_timeout_as_test_user "$effective_timeout" \ + arecord -D "$alt_dev" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" retry_fallback=0 if [ "$rc" -ne 0 ]; then @@ -950,12 +1196,14 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then fallback_ch="$(printf '%s\n' "$combo" | awk '{print $3}')" [ -z "$fmt" ] || [ -z "$fallback_rate" ] || [ -z "$fallback_ch" ] && continue - : > "$record_out" - log_info "[$case_name] fallback: arecord -D \"$alt_dev\" -f $fmt -r $fallback_rate -c $fallback_ch -d $secs_int \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" \ - arecord -D "$alt_dev" -f "$fmt" -r "$fallback_rate" -c "$fallback_ch" -d "$secs_int" "$record_out" >> "$logf" 2>&1 + audio_record_reset_capture_output + log_info "[$case_name] fallback: arecord -D \"$alt_dev\" -f $fmt -r $fallback_rate -c $fallback_ch -d $secs_int \"$record_user_out\"" + audio_run_with_timeout_as_test_user "$effective_timeout" \ + arecord -D "$alt_dev" -f "$fmt" -r "$fallback_rate" -c "$fallback_ch" -d "$secs_int" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" + validation_rate="$fallback_rate" + validation_channels="$fallback_ch" if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then break fi @@ -963,61 +1211,59 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then fi fi - if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 - fi - else # PulseAudio - log_info "[$case_name] exec: parecord --rate=$rate --channels=$channels --file-format=wav \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" parecord --rate="$rate" --channels="$channels" --file-format=wav "$record_out" >> "$logf" 2>&1 + log_info "[$case_name] exec: parecord --rate=$rate --channels=$channels --file-format=wav \"$record_user_out\"" + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" parecord --rate="$rate" --channels="$channels" --file-format=wav "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" # If parecord failed AND PulseAudio control-plane is broken, restart/bootstrap and retry once - if [ "$rc" -ne 0 ] && ! audio_pa_ctl_ok 2>/dev/null; then + if [ "$rc" -ne 0 ] && + ! audio_run_helper_as_test_user --require-session audio_pa_ctl_ok 2>/dev/null; then if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then log_warn "[$case_name] parecord rc=$rc and pactl not responsive - restarting and retrying once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true + audio_record_restart_backend_best_effort "$AUDIO_BACKEND" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session audio_wait_audio_ready 20 "$AUDIO_BACKEND" >/dev/null 2>&1 || true else log_warn "[$case_name] parecord rc=$rc and pactl not responsive - attempting bootstrap and retrying once" - audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true - AUDIO_SYSTEMD_MANAGED=0 - export AUDIO_SYSTEMD_MANAGED - fi - - record_out="$LOGDIR/${case_name}.wav" - : > "$record_out" - log_info "[$case_name] retry: parecord --rate=$rate --channels=$channels --file-format=wav \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" parecord --rate="$rate" --channels="$channels" --file-format=wav "$record_out" >> "$logf" 2>&1 + audio_record_bootstrap_backend_if_needed >/dev/null 2>&1 || true + audio_record_set_recovered_backend_management + fi + + audio_record_set_capture_paths "$case_name" + audio_record_reset_capture_output + log_info "[$case_name] retry: parecord --rate=$rate --channels=$channels --file-format=wav \"$record_user_out\"" + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" parecord --rate="$rate" --channels="$channels" --file-format=wav "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" fi - if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 - fi fi fi + if ! audio_record_promote_capture_output; then + rc=1 + bytes=0 + fi + end_s="$(date +%s 2>/dev/null || echo 0)" last_elapsed=$((end_s - start_s)) [ "$last_elapsed" -lt 0 ] && last_elapsed=0 + audio_record_validate_final_output + # Evidence - pw_ev="$(audio_evidence_pw_streaming || echo 0)" - pa_ev="$(audio_evidence_pa_streaming || echo 0)" + pw_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pw_streaming || echo 0)" + pa_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pa_streaming || echo 0)" if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then - if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + if [ "$recording_valid" -eq 1 ]; then pa_ev=1 fi fi alsa_ev="$(audio_evidence_alsa_running_any || echo 0)" asoc_ev="$(audio_evidence_asoc_path_on || echo 0)" - pwlog_ev="$(audio_evidence_pw_log_seen || echo 0)" + pwlog_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pw_log_seen || echo 0)" if [ "$AUDIO_BACKEND" = "pulseaudio" ]; then pwlog_ev=0 fi @@ -1035,13 +1281,13 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then asoc_ev=1 fi - log_info "[$case_name] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev bytes=${bytes:-0} pw_log=$pwlog_ev" + log_info "[$case_name] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev bytes=${bytes:-0} pw_log=$pwlog_ev validation='${AUDIO_WAV_VALIDATION_SUMMARY:-unavailable}'" - if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s, bytes=$bytes)" + if [ "$recording_valid" -eq 1 ]; then + log_pass "[$case_name] loop $i OK (rc=$rc, ${last_elapsed}s, bytes=$bytes, WAV payload validated)" ok_runs=$((ok_runs + 1)) else - log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}) - see $logf" + log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}, validation='${AUDIO_WAV_VALIDATION_SUMMARY:-unavailable}') - see $logf" fi i=$((i + 1)) @@ -1116,56 +1362,58 @@ else log_info "[$case_name] loop $i/$LOOPS start=$iso secs=$secs backend=$AUDIO_BACKEND $loop_hdr" - record_out="$LOGDIR/${case_name}.wav" - : > "$record_out" + audio_record_set_capture_paths "$case_name" + audio_record_reset_capture_output start_s="$(date +%s 2>/dev/null || echo 0)" + validation_rate=0 + validation_channels=0 if [ "$AUDIO_BACKEND" = "pipewire" ]; then - log_info "[$case_name] exec: pw-record -v \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" pw-record -v "$record_out" >> "$logf" 2>&1 + log_info "[$case_name] exec: pw-record -v \"$record_user_out\"" + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" pw-record -v "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" # If pw-record failed AND PipeWire control-plane is broken, restart/bootstrap and retry once - if [ "$rc" -ne 0 ] && ! audio_pw_ctl_ok 2>/dev/null; then + if [ "$rc" -ne 0 ] && + ! audio_run_helper_as_test_user --require-session audio_pw_ctl_ok 2>/dev/null; then if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - restarting and retrying once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true + audio_record_restart_backend_best_effort "$AUDIO_BACKEND" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session audio_wait_audio_ready 20 "$AUDIO_BACKEND" >/dev/null 2>&1 || true else log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - attempting bootstrap and retrying once" - audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true - AUDIO_SYSTEMD_MANAGED=0 - export AUDIO_SYSTEMD_MANAGED + audio_record_bootstrap_backend_if_needed >/dev/null 2>&1 || true + audio_record_set_recovered_backend_management fi - record_out="$LOGDIR/${case_name}.wav" - : > "$record_out" - log_info "[$case_name] retry: pw-record -v \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" pw-record -v "$record_out" >> "$logf" 2>&1 + audio_record_set_capture_paths "$case_name" + audio_record_reset_capture_output + log_info "[$case_name] retry: pw-record -v \"$record_user_out\"" + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" pw-record -v "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" fi - # If we already got real audio, accept and skip fallbacks + # A non-trivial file skips compatibility fallbacks. Content and + # recorder status are validated after the backend flow completes. if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - if [ "$rc" -ne 0 ]; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 - fi - else + : + else # Only if output is tiny/empty do we try a virtual PCM if command -v arecord >/dev/null 2>&1; then - pcm="$(alsa_pick_virtual_pcm || true)" + pcm="$(audio_run_helper_as_test_user alsa_pick_virtual_pcm || true)" if [ -n "$pcm" ]; then secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)" [ -z "$secs_int" ] && secs_int=0 - : > "$record_out" - log_info "[$case_name] fallback: arecord -D $pcm -f S16_LE -r 48000 -c 2 -d $secs_int \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" \ - arecord -D "$pcm" -f S16_LE -r 48000 -c 2 -d "$secs_int" "$record_out" >> "$logf" 2>&1 + audio_record_reset_capture_output + log_info "[$case_name] fallback: arecord -D $pcm -f S16_LE -r 48000 -c 2 -d $secs_int \"$record_user_out\"" + audio_run_with_timeout_as_test_user "$effective_timeout" \ + arecord -D "$pcm" -f S16_LE -r 48000 -c 2 -d "$secs_int" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" + validation_rate=48000 + validation_channels=2 fi fi @@ -1179,29 +1427,25 @@ else fi fi if [ "$retry_target" -eq 1 ] && [ -n "$SRC_ID" ]; then - : > "$record_out" - log_info "[$case_name] exec: pw-record -v --target \"$SRC_ID\" \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" pw-record -v --target "$SRC_ID" "$record_out" >> "$logf" 2>&1 + audio_record_reset_capture_output + log_info "[$case_name] exec: pw-record -v --target \"$SRC_ID\" \"$record_user_out\"" + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" pw-record -v --target "$SRC_ID" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" fi fi - # Optional safety: If nonzero rc but output is clearly valid, accept - if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 - fi - else if [ "$AUDIO_BACKEND" = "alsa" ]; then secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)" [ -z "$secs_int" ] && secs_int=0 - log_info "[$case_name] exec: arecord -D \"$SRC_ID\" -f S16_LE -r 48000 -c 2 -d $secs_int \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" \ - arecord -D "$SRC_ID" -f S16_LE -r 48000 -c 2 -d "$secs_int" "$record_out" >> "$logf" 2>&1 + log_info "[$case_name] exec: arecord -D \"$SRC_ID\" -f S16_LE -r 48000 -c 2 -d $secs_int \"$record_user_out\"" + audio_run_with_timeout_as_test_user "$effective_timeout" \ + arecord -D "$SRC_ID" -f S16_LE -r 48000 -c 2 -d "$secs_int" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" + validation_rate=48000 + validation_channels=2 retry_alsa=0 if [ "$rc" -ne 0 ]; then @@ -1230,73 +1474,73 @@ else ch="$(printf '%s\n' "$combo" | awk '{print $3}')" [ -z "$fmt" ] || [ -z "$rate" ] || [ -z "$ch" ] && continue - : > "$record_out" - log_info "[$case_name] retry: arecord -D \"$alt_dev\" -f $fmt -r $rate -c $ch -d $secs_int \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" \ - arecord -D "$alt_dev" -f "$fmt" -r "$rate" -c "$ch" -d "$secs_int" "$record_out" >> "$logf" 2>&1 + audio_record_reset_capture_output + log_info "[$case_name] retry: arecord -D \"$alt_dev\" -f $fmt -r $rate -c $ch -d $secs_int \"$record_user_out\"" + audio_run_with_timeout_as_test_user "$effective_timeout" \ + arecord -D "$alt_dev" -f "$fmt" -r "$rate" -c "$ch" -d "$secs_int" "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" + validation_rate="$rate" + validation_channels="$ch" if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then break fi done fi - if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 - fi - else # PulseAudio - log_info "[$case_name] exec: parecord --file-format=wav \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" parecord --file-format=wav "$record_out" >> "$logf" 2>&1 + log_info "[$case_name] exec: parecord --file-format=wav \"$record_user_out\"" + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" parecord --file-format=wav "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" # If parecord failed AND PulseAudio control-plane is broken, restart/bootstrap and retry once - if [ "$rc" -ne 0 ] && ! audio_pa_ctl_ok 2>/dev/null; then + if [ "$rc" -ne 0 ] && + ! audio_run_helper_as_test_user --require-session audio_pa_ctl_ok 2>/dev/null; then if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then log_warn "[$case_name] parecord rc=$rc and pactl not responsive - restarting and retrying once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true + audio_record_restart_backend_best_effort "$AUDIO_BACKEND" >/dev/null 2>&1 || true + audio_run_helper_as_test_user --require-session audio_wait_audio_ready 20 "$AUDIO_BACKEND" >/dev/null 2>&1 || true else log_warn "[$case_name] parecord rc=$rc and pactl not responsive - attempting bootstrap and retrying once" - audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true - AUDIO_SYSTEMD_MANAGED=0 - export AUDIO_SYSTEMD_MANAGED + audio_record_bootstrap_backend_if_needed >/dev/null 2>&1 || true + audio_record_set_recovered_backend_management fi - record_out="$LOGDIR/${case_name}.wav" - : > "$record_out" - log_info "[$case_name] retry: parecord --file-format=wav \"$record_out\"" - audio_exec_with_timeout "$effective_timeout" parecord --file-format=wav "$record_out" >> "$logf" 2>&1 + audio_record_set_capture_paths "$case_name" + audio_record_reset_capture_output + log_info "[$case_name] retry: parecord --file-format=wav \"$record_user_out\"" + audio_run_with_timeout_as_test_user --require-session "$effective_timeout" parecord --file-format=wav "$record_user_out" >> "$logf" 2>&1 rc=$? - bytes="$(file_size_bytes "$record_out" 2>/dev/null || echo 0)" + bytes="$(audio_record_capture_size)" fi - if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 - fi fi fi + if ! audio_record_promote_capture_output; then + rc=1 + bytes=0 + fi + end_s="$(date +%s 2>/dev/null || echo 0)" last_elapsed=$((end_s - start_s)) [ "$last_elapsed" -lt 0 ] && last_elapsed=0 + audio_record_validate_final_output + # Evidence - pw_ev="$(audio_evidence_pw_streaming || echo 0)" - pa_ev="$(audio_evidence_pa_streaming || echo 0)" + pw_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pw_streaming || echo 0)" + pa_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pa_streaming || echo 0)" if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then - if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + if [ "$recording_valid" -eq 1 ]; then pa_ev=1 fi fi alsa_ev="$(audio_evidence_alsa_running_any || echo 0)" asoc_ev="$(audio_evidence_asoc_path_on || echo 0)" - pwlog_ev="$(audio_evidence_pw_log_seen || echo 0)" + pwlog_ev="$(audio_run_helper_as_test_user --require-session audio_evidence_pw_log_seen || echo 0)" if [ "$AUDIO_BACKEND" = "pulseaudio" ]; then pwlog_ev=0 fi @@ -1314,13 +1558,13 @@ else asoc_ev=1 fi - log_info "[$case_name] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev bytes=${bytes:-0} pw_log=$pwlog_ev" + log_info "[$case_name] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev bytes=${bytes:-0} pw_log=$pwlog_ev validation='${AUDIO_WAV_VALIDATION_SUMMARY:-unavailable}'" - if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s, bytes=$bytes)" + if [ "$recording_valid" -eq 1 ]; then + log_pass "[$case_name] loop $i OK (rc=$rc, ${last_elapsed}s, bytes=$bytes, WAV payload validated)" ok_runs=$((ok_runs + 1)) else - log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}) - see $logf" + log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}, validation='${AUDIO_WAV_VALIDATION_SUMMARY:-unavailable}') - see $logf" fi i=$((i + 1)) @@ -1355,7 +1599,7 @@ fi # Collect evidence once at end if [ "$DMESG_SCAN" -eq 1 ]; then scan_audio_dmesg "$LOGDIR" - dump_mixers "$LOGDIR/mixer_dump.txt" + audio_record_dump_mixers "$LOGDIR/mixer_dump.txt" fi # JUnit finalize (optional) @@ -1395,3 +1639,4 @@ fi log_fail "$TESTNAME FAIL" echo "$RESULT_TESTNAME FAIL" > "$RES_FILE" exit 1 + From d202dfaf183b1943556d76c842fade4138f5d522 Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Sat, 25 Jul 2026 05:59:39 +0530 Subject: [PATCH 4/4] audio: validate card device access as Debian user Keep ALSA card discovery, inventory, diagnostics, and reporting under root while validating device-node access as the Debian audio user. - remove complete-runner re-execution through runuser - prepare Debian audio-group membership without starting PipeWire - validate control, playback, and capture PCM nodes as the Debian user - keep the matched-card inventory and test results root-owned - preserve base and AudioReach overlay package preparation - retain ALSA inventory and audio-focused dmesg validation Signed-off-by: Srikanth Muppandam --- .../Audio/Audio_Card_Registration/run.sh | 172 ++++++++++++++++-- 1 file changed, 154 insertions(+), 18 deletions(-) diff --git a/Runner/suites/Multimedia/Audio/Audio_Card_Registration/run.sh b/Runner/suites/Multimedia/Audio/Audio_Card_Registration/run.sh index ca298e507..3e2531fc6 100755 --- a/Runner/suites/Multimedia/Audio/Audio_Card_Registration/run.sh +++ b/Runner/suites/Multimedia/Audio/Audio_Card_Registration/run.sh @@ -6,6 +6,7 @@ # - validates ALSA sound card registration # - validates /dev/snd/controlC nodes # - optionally validates PCM/playback/capture entries +# - optionally prepares Debian AudioReach packages with --overlay # - does not start/restart PipeWire, PulseAudio, ADSP, or remoteproc # - does not play or record audio @@ -41,8 +42,18 @@ fi # shellcheck disable=SC1091 . "$TOOLS/audio_common.sh" +if [ -r "$TOOLS/lib_pkg_provider.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$TOOLS/lib_pkg_provider.sh" +fi + TESTNAME="Audio_Card_Registration" +# Only the explicit --overlay option enables Debian AudioReach preparation. +# Ignore inherited values so native/base mode remains the default. +AUDIO_OVERLAY_REQUESTED=0 +AUDIO_EARLY_HELP_REQUESTED=0 + AUDIO_CARD_WAIT_SECS="${AUDIO_CARD_WAIT_SECS:-30}" AUDIO_CARD_REQUIRED="${AUDIO_CARD_REQUIRED:-auto}" AUDIO_CARD_MATCH="${AUDIO_CARD_MATCH:-}" @@ -54,10 +65,15 @@ DMESG_SCAN="${DMESG_SCAN:-1}" VERBOSE="${VERBOSE:-0}" usage() { - cat </dev/null 2>&1; then + log_fail "$TESTNAME FAIL: required helper is unavailable: audio_prepare_test_packages" + echo "$TESTNAME FAIL" > "$RES_FILE" + exit 1 +fi + +audio_prepare_test_packages "$AUDIO_OVERLAY_REQUESTED" +audio_prepare_rc=$? + +case "$audio_prepare_rc" in + 0) + ;; + 2) + log_skip "$TESTNAME SKIP: AudioReach kernel package changed; reboot required" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + ;; + *) + log_fail "$TESTNAME FAIL: audio package preparation failed" + echo "$TESTNAME FAIL" > "$RES_FILE" + exit 1 + ;; +esac + +# Prepare only the Debian Audio account and group membership. This ALSA-only +# testcase does not require a systemd user manager and does not re-execute the +# complete runner as debian. +if ! command -v audio_prepare_debian_audio_environment >/dev/null 2>&1; then + log_fail "$TESTNAME FAIL: required helper is unavailable: audio_prepare_debian_audio_environment" + echo "$TESTNAME FAIL" > "$RES_FILE" + exit 1 +fi + +if ! audio_prepare_debian_audio_environment 0; then + log_fail "$TESTNAME FAIL: Debian Audio environment preparation failed" + echo "$TESTNAME FAIL" > "$RES_FILE" + exit 1 +fi + +if ! command -v audio_run_helper_as_test_user >/dev/null 2>&1; then + log_fail "$TESTNAME FAIL: required helper is unavailable: audio_run_helper_as_test_user" + echo "$TESTNAME FAIL" > "$RES_FILE" + exit 1 +fi + +if ! mkdir -p "$LOGDIR"; then + log_fail "$TESTNAME FAIL: failed to create log directory: $LOGDIR" + echo "$TESTNAME FAIL" > "$RES_FILE" + exit 1 +fi + +if ! chmod 0755 "$LOGDIR"; then + log_fail "$TESTNAME FAIL: failed to make log directory readable for ALSA user validation" + echo "$TESTNAME FAIL" > "$RES_FILE" + exit 1 +fi + +if ! : > "$RES_FILE"; then + log_fail "$TESTNAME FAIL: failed to initialize result file: $RES_FILE" + exit 1 +fi + while [ $# -gt 0 ]; do case "$1" in + --overlay) + AUDIO_OVERLAY_REQUESTED=1 + export AUDIO_OVERLAY_REQUESTED + shift + ;; --wait-secs) if [ $# -lt 2 ]; then echo "[ERROR] --wait-secs requires an argument" >&2 @@ -260,24 +373,24 @@ esac test_path="$(find_test_case_by_name "$TESTNAME" 2>/dev/null || echo "$SCRIPT_DIR")" if ! cd "$test_path"; then log_fail "cd failed: $test_path" + echo "$TESTNAME FAIL" > "$RES_FILE" exit 1 fi -RES_FILE="./$TESTNAME.res" -LOGDIR="./results/$TESTNAME" - -mkdir -p "$LOGDIR" 2>/dev/null || true -: > "$RES_FILE" - -if ! CHECK_DEPS_NO_EXIT=1 check_dependencies awk grep sed cat ls find sleep wc; then +if ! CHECK_DEPS_NO_EXIT=1 check_dependencies awk grep sed cat ls find sleep wc chmod; then log_skip "$TESTNAME SKIP: missing required dependencies" echo "$TESTNAME SKIP" > "$RES_FILE" exit 0 fi +# This testcase intentionally performs no PipeWire/PulseAudio runtime setup. +# Do not call audio_prepare_overlay_runtime(), audio_restart_pipewire_service(), +# or audio_restart_services_best_effort() here. Only the ALSA node validation +# helpers below are executed as the Debian Audio user. + log_info "--------------------------------------------------------------------------" log_info "------------------- Starting $TESTNAME Testcase --------------------------" -log_info "Config, AUDIO_CARD_WAIT_SECS=$AUDIO_CARD_WAIT_SECS AUDIO_CARD_REQUIRED=$AUDIO_CARD_REQUIRED AUDIO_CARD_MATCH=${AUDIO_CARD_MATCH:-}" +log_info "Config, AUDIO_OVERLAY_REQUESTED=$AUDIO_OVERLAY_REQUESTED AUDIO_CARD_WAIT_SECS=$AUDIO_CARD_WAIT_SECS AUDIO_CARD_REQUIRED=$AUDIO_CARD_REQUIRED AUDIO_CARD_MATCH=${AUDIO_CARD_MATCH:-}" log_info "Config, REQUIRE_CONTROL_NODE=$REQUIRE_CONTROL_NODE REQUIRE_PCM_NODE=$REQUIRE_PCM_NODE REQUIRE_PLAYBACK_PCM=$REQUIRE_PLAYBACK_PCM REQUIRE_CAPTURE_PCM=$REQUIRE_CAPTURE_PCM DMESG_SCAN=$DMESG_SCAN" if command -v detect_platform >/dev/null 2>&1; then @@ -344,7 +457,18 @@ if ! audio_card_wait_for_cards "$AUDIO_CARD_WAIT_SECS" "$AUDIO_CARD_MATCH"; then fi MATCHED_CARDS_FILE="$LOGDIR/matched_cards.txt" -: > "$MATCHED_CARDS_FILE" + +if ! : > "$MATCHED_CARDS_FILE"; then + log_fail "$TESTNAME FAIL: failed to initialize matched card inventory" + echo "$TESTNAME FAIL" > "$RES_FILE" + exit 0 +fi + +if ! chmod 0644 "$MATCHED_CARDS_FILE"; then + log_fail "$TESTNAME FAIL: failed to make matched card inventory readable for the Audio user" + echo "$TESTNAME FAIL" > "$RES_FILE" + exit 0 +fi if [ -n "$AUDIO_CARD_MATCH" ]; then audio_card_find_matching_cards "$AUDIO_CARD_MATCH" > "$MATCHED_CARDS_FILE" @@ -367,7 +491,9 @@ done < "$MATCHED_CARDS_FILE" test_failed=0 if [ "$REQUIRE_CONTROL_NODE" -eq 1 ]; then - if ! audio_card_validate_control_nodes "$MATCHED_CARDS_FILE"; then + if ! audio_run_helper_as_test_user \ + audio_card_validate_control_nodes \ + "$MATCHED_CARDS_FILE"; then test_failed=1 fi else @@ -375,7 +501,10 @@ else fi if [ "$REQUIRE_PCM_NODE" -eq 1 ]; then - if ! audio_card_validate_pcm_nodes "$MATCHED_CARDS_FILE" "any"; then + if ! audio_run_helper_as_test_user \ + audio_card_validate_pcm_nodes \ + "$MATCHED_CARDS_FILE" \ + "any"; then test_failed=1 fi else @@ -383,7 +512,10 @@ else fi if [ "$REQUIRE_PLAYBACK_PCM" -eq 1 ]; then - if ! audio_card_validate_pcm_nodes "$MATCHED_CARDS_FILE" "playback"; then + if ! audio_run_helper_as_test_user \ + audio_card_validate_pcm_nodes \ + "$MATCHED_CARDS_FILE" \ + "playback"; then test_failed=1 fi else @@ -391,7 +523,10 @@ else fi if [ "$REQUIRE_CAPTURE_PCM" -eq 1 ]; then - if ! audio_card_validate_pcm_nodes "$MATCHED_CARDS_FILE" "capture"; then + if ! audio_run_helper_as_test_user \ + audio_card_validate_pcm_nodes \ + "$MATCHED_CARDS_FILE" \ + "capture"; then test_failed=1 fi else @@ -412,3 +547,4 @@ fi log_info "------------------- Completed $TESTNAME Testcase --------------------------" exit 0 +