From b251b98905275dc33a6a35747f079efd4457f0aa Mon Sep 17 00:00:00 2001 From: Teja Swaroop Moida Date: Mon, 15 Sep 2025 14:59:01 +0530 Subject: [PATCH] Introduce unified audio test workflow and enhancements - Modular runner supports both PipeWire and PulseAudio backends - Validation improved with evidence-based checks for playback and recording - Automatic handling of audio assets (download and extraction) - Enhanced diagnostics: dmesg scan, mixer dumps, JUnit XML output for CI - Added CLI options and environment variable support for configuration - Utility scripts refactored for better extensibility and maintainability Signed-off-by: Teja Swaroop Moida --- .../Multimedia/Audio/AudioPlayback/Read_me.md | 139 +++-- .../Multimedia/Audio/AudioPlayback/run.sh | 324 ++++++++--- .../Multimedia/Audio/AudioRecord/Read_me.md | 134 ++++- .../Multimedia/Audio/AudioRecord/run.sh | 326 +++++++++-- Runner/utils/audio_common.sh | 549 ++++++++++++++++++ Runner/utils/functestlib.sh | 69 ++- 6 files changed, 1349 insertions(+), 192 deletions(-) create mode 100755 Runner/utils/audio_common.sh diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md b/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md index 86bd8a60..2a9701d8 100644 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md @@ -1,41 +1,55 @@ -# Audio playback Validation Script for Qualcomm Linux based platform (Yocto) +# Audio Playback Validation Script for Qualcomm Linux-based Platform (Yocto) ## Overview -This script automates the validation of audio playback capabilities on the Qualcomm Linux based platform running a Yocto-based Linux system. It utilizes pulseaudio test app to decode wav file. +This suite automates the validation of audio playback capabilities on Qualcomm Linux-based platforms running a Yocto-based Linux system. It supports both PipeWire and PulseAudio backends, with robust evidence-based PASS/FAIL logic, asset management, and diagnostic logging. + ## Features -- Decoding PCM clip -- Compatible with Yocto-based root filesystem + +- Supports **PipeWire** and **PulseAudio** backends +- Plays audio clips with configurable format, duration, and loop count +- Automatically downloads and extracts audio assets if missing +- Validates playback using multiple evidence sources: + - PipeWire/PulseAudio streaming state + - ALSA and ASoC runtime status + - Kernel logs (`dmesg`) +- Diagnostic logs: dmesg scan, mixer dumps, playback logs +- Evidence-based validation (user-space, ALSA, ASoC, dmesg) +- Generates `.res` result file and optional JUnit XML output + ## Prerequisites Ensure the following components are present in the target Yocto build: -- `paplay` binary(available at /usr/bin) +- PipeWire: `pw-play`, `wpctl` +- PulseAudio: `paplay`, `pactl` +- Common tools: `pgrep`, `timeout`, `grep`, `wget`, `tar` +- Daemon: `pipewire` or `pulseaudio` must be running ## Directory Structure ```bash Runner/ -├──suites/ -├ ├── Multimedia/ -│ ├ ├── Audio/ -│ ├ ├ ├── AudioPlayback/ -│ ├ ├ ├ ├ └── run.sh -├ ├ ├ ├ ├ └── Read_me.md +├── run-test.sh +├── utils/ +│ ├── functestlib.sh +│ └── audio_common.sh +└── suites/ + └── Multimedia/ + └── Audio/ + ├── AudioPlayback/ + ├── run.sh + └── Read_me.md ``` ## Usage - -Instructions - +Instructions: 1. Copy repo to Target Device: Use scp to transfer the scripts from the host to the target device. The scripts should be copied to any directory on the target device. - -2. Verify Transfer: Ensure that the repo have been successfully copied to any directory on the target device. - +2. Verify Transfer: Ensure that the repo has been successfully copied to any directory on the target device. 3. Run Scripts: Navigate to the directory where these files are copied on the target device and execute the scripts as needed. Run a specific test using: @@ -46,33 +60,92 @@ git clone cd scp -r Runner user@target_device_ip: ssh user@target_device_ip -cd /Runner && ./run-test.sh AudioPlayback + +**Using Unified Runner** +cd /Runner +# Run AudioPlayback using PipeWire (auto-detects backend if not specified) +./run-test.sh AudioPlayback +# Force PulseAudio backend +AUDIO_BACKEND=pulseaudio ./run-test.sh AudioPlayback +# Custom options via environment variables +AUDIO_BACKEND=pipewire PLAYBACK_TIMEOUT=20s PLAYBACK_LOOPS=2 ./run-test.sh AudioPlayback + +**Directly from Test Directory** +cd Runner/suites/Multimedia/Audio/AudioPlayback +# Show usage/help +./run.sh --help +# Run with PipeWire, 3 loops, 10s timeout, speakers sink +./run.sh --backend pipewire --sink speakers --loops 3 --timeout 10s +# Run with PulseAudio, null sink, strict mode, verbose +./run.sh --backend pulseaudio --sink null --strict --verbose + +Environment Variables: +Variable Description Default +AUDIO_BACKEND Selects backend: pipewire or pulseaudio auto-detect +SINK_CHOICE Playback sink: speakers or null speakers +FORMATS Audio formats: e.g. wav wav +DURATIONS Playback durations: short, medium, long short +LOOPS Number of playback loops 1 +TIMEOUT Playback timeout per loop (e.g., 15s, 0=none) 0 +STRICT Enable strict mode (fail on any error) 0 +DMESG_SCAN Scan dmesg for errors after playback 1 +VERBOSE Enable verbose logging 0 +EXTRACT_AUDIO_ASSETS Download/extract audio assets if missing true +JUNIT_OUT Path to write JUnit XML output unset + +CLI Options +Option Description +--backend Select backend: pipewire or pulseaudio +--sink Playback sink: speakers or null +--formats Audio formats (space/comma separated): e.g. wav +--durations Playback durations: short, medium, long +--loops Number of playback loops +--timeout Playback timeout per loop (e.g., 15s) +--strict Enable strict mode +--no-dmesg Disable dmesg scan +--no-extract-assets Disable asset extraction +--junit Write JUnit XML output +--verbose Enable verbose logging +--help Show usage instructions + ``` + Sample Output: ``` -sh-5.2# cd /Runner/ && ./run-test.sh AudioPlayback -[Executing test case: AudioPlayback] 2025-05-28 19:01:59 - -[INFO] 2025-05-28 19:01:59 - ------------------------------------------------------------ -[INFO] 2025-05-28 19:01:59 - ------------------- Starting AudioPlayback Testcase ------------ -[INFO] 2025-05-28 19:01:59 - Checking if dependency binary is available -[INFO] 2025-05-28 19:01:59 - Playback clip present: AudioClips/yesterday_48KHz.wav -[PASS] 2025-05-28 19:02:14 - Playback completed or timed out (ret=124) as expected. -[PASS] 2025-05-28 19:02:14 - AudioPlayback : Test Passed -[INFO] 2025-05-28 19:02:14 - See results/audioplayback/playback_stdout.log, dmesg_before/after.log, syslog_before/after.log for debug details -[INFO] 2025-05-28 19:02:14 - ------------------- Completed AudioPlayback Testcase ------------- -[PASS] 2025-05-28 19:02:14 - AudioPlayback passed -sh-5.2# +sh-5.3# ./run.sh --backend pipewire +[INFO] 2025-09-12 05:24:47 - ---------------- Starting AudioPlayback ---------------- +[INFO] 2025-09-12 05:24:47 - SoC: 498 +[INFO] 2025-09-12 05:24:47 - Args: backend=pipewire sink=speakers loops=1 timeout=0 formats='wav' durations='short' strict=0 dmesg=1 extract=true +[INFO] 2025-09-12 05:24:47 - Using backend: pipewire +[INFO] 2025-09-12 05:24:47 - Routing to sink: id=72 name='Built-in Audio Speaker playback' choice=speakers +[INFO] 2025-09-12 05:24:47 - Watchdog/timeout: 0 +[INFO] 2025-09-12 05:24:47 - [play_wav_short] loop 1/1 start=2025-09-12T05:24:47Z clip=AudioClips/yesterday_48KHz.wav backend=pipewire sink=speakers(72) +[INFO] 2025-09-12 05:24:47 - [play_wav_short] exec: pw-play -v "AudioClips/yesterday_48KHz.wav" +[INFO] 2025-09-12 05:26:52 - [play_wav_short] evidence: pw_streaming=1 pa_streaming=0 alsa_running=1 asoc_path_on=1 pw_log=1 +[PASS] 2025-09-12 05:26:52 - [play_wav_short] loop 1 OK (rc=0, 125s) +[INFO] 2025-09-12 05:26:52 - Scanning dmesg for snd|audio|pipewire|pulseaudio: errors & success patterns +[INFO] 2025-09-12 05:26:52 - No snd|audio|pipewire|pulseaudio-related errors found (no OK pattern requested) +[INFO] 2025-09-12 05:26:52 - Summary: total=1 pass=1 fail=0 skip=0 +[PASS] 2025-09-12 05:26:52 - AudioPlayback PASS ``` -3. Results will be available in the `Runner/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.res` directory. + +Results: +Results are stored in: results/AudioPlayback/ +Summary result file: AudioPlayback.res +JUnit XML (if enabled): .xml +Diagnostic logs: dmesg snapshots, mixer dumps, playback logs per test case ## Notes -- The script does not take any arguments. -- It validates the presence of required libraries before executing tests. +- The script validates the presence of required tools before executing tests; missing tools result in SKIP. - If any critical tool is missing, the script exits with an error message. +- Logs include dmesg snapshots, mixer dumps, and playback logs. +- Asset download requires network connectivity. +- Evidence-based PASS/FAIL logic ensures reliability even if backend quirks occur. ## License SPDX-License-Identifier: BSD-3-Clause-Clear (C) Qualcomm Technologies, Inc. and/or its subsidiaries. + diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh index 7c71f620..c684c6f8 100755 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh @@ -1,94 +1,282 @@ #!/bin/sh - # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. # SPDX-License-Identifier: BSD-3-Clause-Clear -# --------- Robustly source init_env and functestlib.sh ---------- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# ---- Source init_env & tools ---- INIT_ENV="" SEARCH="$SCRIPT_DIR" while [ "$SEARCH" != "/" ]; do - if [ -f "$SEARCH/init_env" ]; then - INIT_ENV="$SEARCH/init_env" - break - fi - SEARCH=$(dirname "$SEARCH") + if [ -f "$SEARCH/init_env" ]; then INIT_ENV="$SEARCH/init_env"; break; fi + SEARCH=$(dirname "$SEARCH") done - -if [ -z "$INIT_ENV" ]; then - echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 - exit 1 -fi - -if [ -z "$__INIT_ENV_LOADED" ]; then - # shellcheck disable=SC1090 - . "$INIT_ENV" -fi +[ -z "$INIT_ENV" ] && echo "[ERROR] init_env not found" >&2 && exit 1 +# shellcheck disable=SC1090 +[ -z "$__INIT_ENV_LOADED" ] && . "$INIT_ENV" # shellcheck disable=SC1090,SC1091 . "$TOOLS/functestlib.sh" -# --------------------------------------------------------------- +. "$TOOLS/audio_common.sh" TESTNAME="AudioPlayback" -TESTBINARY="paplay" -TAR_URL="https://github.com/qualcomm-linux/qcom-linux-testkit/releases/download/Pulse-Audio-Files-v1.0/AudioClips.tar.gz" -PLAYBACK_CLIP="AudioClips/yesterday_48KHz.wav" -AUDIO_DEVICE="low-latency0" -LOGDIR="results/audioplayback" -RESULT_FILE="$TESTNAME.res" +RES_FILE="./${TESTNAME}.res" +LOGDIR="results/${TESTNAME}" +mkdir -p "$LOGDIR" -test_path=$(find_test_case_by_name "$TESTNAME") -cd "$test_path" || exit 1 +# ---- Assets ---- +AUDIO_TAR_URL="${AUDIO_TAR_URL:-https://github.com/qualcomm-linux/qcom-linux-testkit/releases/download/Pulse-Audio-Files-v1.0/AudioClips.tar.gz}" +export AUDIO_TAR_URL -# Prepare logdir -mkdir -p "$LOGDIR" -chmod -R 777 "$LOGDIR" - -log_info "------------------------------------------------------------" -log_info "------------------- Starting $TESTNAME Testcase ------------" - -log_info "Checking if dependency binary is available" -check_dependencies "$TESTBINARY" pgrep grep timeout - -# Download/extract audio if not present -if [ ! -f "$PLAYBACK_CLIP" ]; then - log_info "Audio clip not found, downloading..." - extract_tar_from_url "$TAR_URL" || { - log_fail "Failed to fetch/extract playback audio tarball" - echo "$TESTNAME FAIL" > "$RESULT_FILE" - exit 1 - } +# ------------- Defaults / CLI ------------- +AUDIO_BACKEND="" +SINK_CHOICE="${SINK_CHOICE:-speakers}" # speakers|null +FORMATS="${FORMATS:-wav}" +DURATIONS="${DURATIONS:-short}" # short|medium|long +LOOPS="${LOOPS:-1}" +TIMEOUT="${TIMEOUT:-0}" # 0 = no timeout (recommended) +STRICT="${STRICT:-0}" +DMESG_SCAN="${DMESG_SCAN:-1}" +VERBOSE=0 +EXTRACT_AUDIO_ASSETS="${EXTRACT_AUDIO_ASSETS:-true}" +JUNIT_OUT="" + +usage() { + cat </dev/null || echo "$SCRIPT_DIR")" +cd "$test_path" || { log_error "cd failed: $test_path"; echo "$TESTNAME FAIL" >"$RES_FILE"; exit 1; } + +log_info "---------------- Starting $TESTNAME ----------------" +log_info "SoC: $(soc_id)" +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" + +# Resolve backend +[ -z "$AUDIO_BACKEND" ] && AUDIO_BACKEND="$(detect_audio_backend)" +if [ -z "$AUDIO_BACKEND" ]; then + log_skip "$TESTNAME SKIP - no audio backend running" + echo "$TESTNAME SKIP" >"$RES_FILE"; exit 0 fi +log_info "Using backend: $AUDIO_BACKEND" -if [ ! -f "$PLAYBACK_CLIP" ]; then - log_fail "Playback clip $PLAYBACK_CLIP not found after extraction." - echo "$TESTNAME : FAIL" > "$RESULT_FILE" - exit 1 +if ! check_audio_daemon "$AUDIO_BACKEND"; then + log_skip "$TESTNAME SKIP - backend not available: $AUDIO_BACKEND" + echo "$TESTNAME SKIP" >"$RES_FILE"; exit 0 fi -log_info "Playback clip present: $PLAYBACK_CLIP" +# Dependencies per backend +if [ "$AUDIO_BACKEND" = "pipewire" ]; then + check_dependencies wpctl pw-play || { log_skip "$TESTNAME SKIP - missing PipeWire utils"; echo "$TESTNAME SKIP" >"$RES_FILE"; exit 0; } +else + check_dependencies pactl paplay || { log_skip "$TESTNAME SKIP - missing PulseAudio utils"; echo "$TESTNAME SKIP" >"$RES_FILE"; exit 0; } +fi -# --- Capture logs BEFORE playback (for debugging) --- -dmesg > "$LOGDIR/dmesg_before.log" +# ----- Route sink (set default; player uses default sink) ----- +SINK_ID="" +case "$AUDIO_BACKEND:$SINK_CHOICE" in + pipewire:null) SINK_ID="$(pw_default_null)" ;; + pipewire:*) SINK_ID="$(pw_default_speakers)" ;; + pulseaudio:null) SINK_ID="$(pa_default_null)" ;; + pulseaudio:*) SINK_ID="$(pa_default_speakers)" ;; +esac +if [ -z "$SINK_ID" ]; then + log_skip "$TESTNAME SKIP - requested sink '$SINK_CHOICE' not found for $AUDIO_BACKEND" + echo "$TESTNAME SKIP" >"$RES_FILE"; exit 0 +fi -# --- Start the Playback, capture output --- -timeout 15s paplay "$PLAYBACK_CLIP" -d "$AUDIO_DEVICE" > "$LOGDIR/playback_stdout.log" 2>&1 -ret=$? +if [ "$AUDIO_BACKEND" = "pipewire" ]; then + SINK_NAME="$(pw_sink_name_safe "$SINK_ID")" + wpctl set-default "$SINK_ID" >/dev/null 2>&1 || true + [ -n "$SINK_NAME" ] || SINK_NAME="unknown" + log_info "Routing to sink: id=$SINK_ID name='$SINK_NAME' choice=$SINK_CHOICE" +else + SINK_NAME="$(pa_sink_name "$SINK_ID")" + [ -n "$SINK_NAME" ] || SINK_NAME="$SINK_ID" + pa_set_default_sink "$SINK_ID" >/dev/null 2>&1 || true + log_info "Routing to sink: name='$SINK_NAME' choice=$SINK_CHOICE" +fi -# --- Capture logs AFTER playback (for debugging) --- -dmesg > "$LOGDIR/dmesg_after.log" +# JUnit init +if [ -n "$JUNIT_OUT" ]; then JUNIT_TMP="$LOGDIR/.junit_cases.xml"; : > "$JUNIT_TMP"; fi +append_junit() { + name="$1"; elapsed="$2"; status="$3"; logf="$4" + [ -n "$JUNIT_OUT" ] || return 0 + safe_msg="$(tail -n 50 "$logf" 2>/dev/null | sed 's/&/\&/g;s//\>/g')" + { + printf ' \n' "Audio.Playback" "$name" "$elapsed" + case "$status" in + PASS) : ;; + SKIP) printf ' \n' ;; + FAIL) printf ' \n' "failed"; printf '%s\n' "$safe_msg"; printf ' \n' ;; + esac + printf ' \n' + } >> "$JUNIT_TMP" +} -if [ "$ret" -eq 0 ] || [ "$ret" -eq 124 ] ; then - log_pass "Playback completed or timed out (ret=$ret) as expected." - log_pass "$TESTNAME : Test Passed" - echo "$TESTNAME PASS" > "$RESULT_FILE" - exit 0 +# Decide minimum ok seconds if timeout>0 +dur_s="$(duration_to_secs "$TIMEOUT" 2>/dev/null || echo 0)" +[ -z "$dur_s" ] && dur_s=0 +min_ok=0 +if [ "$dur_s" -gt 0 ] 2>/dev/null; then + min_ok=$((dur_s - 1)); [ "$min_ok" -lt 1 ] && min_ok=1 + log_info "Watchdog/timeout: ${TIMEOUT}" else - log_fail "$TESTBINARY playback exited with error code $ret" - log_fail "$TESTNAME : Test Failed" - echo "$TESTNAME FAIL" > "$RESULT_FILE" - exit 1 + log_info "Watchdog/timeout: disabled (no timeout)" fi -log_info "See $LOGDIR/playback_stdout.log, dmesg_before/after.log, syslog_before/after.log for debug details" -log_info "------------------- Completed $TESTNAME Testcase -------------" -exit 0 +# ------------- Matrix execution ------------- +total=0; pass=0; fail=0; skip=0; suite_rc=0 + +for fmt in $FORMATS; do + for dur in $DURATIONS; do + clip="$(resolve_clip "$fmt" "$dur")" + case_name="play_${fmt}_${dur}" + total=$((total + 1)) + logf="$LOGDIR/${case_name}.log"; : > "$logf" + export AUDIO_LOGCTX="$logf" + + 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" + append_junit "$case_name" "0" "SKIP" "$logf"; skip=$((skip + 1)); continue + fi + + if [ "${EXTRACT_AUDIO_ASSETS:-true}" = "true" ]; then + ensure_playback_clip "$clip" || { + log_warn "[$case_name] Clip missing and could not be fetched: $clip" + echo "$case_name SKIP (clip missing)" >> "$LOGDIR/summary.txt" + append_junit "$case_name" "0" "SKIP" "$logf"; skip=$((skip + 1)); continue + } + fi + + i=1; ok_runs=0; last_elapsed=0 + while [ "$i" -le "$LOOPS" ]; do + iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + loop_hdr="sink=$SINK_CHOICE($SINK_ID)" + else + loop_hdr="sink=$SINK_CHOICE($SINK_NAME)" + fi + log_info "[$case_name] loop $i/$LOOPS start=$iso clip=$clip backend=$AUDIO_BACKEND $loop_hdr" + start_s="$(date +%s 2>/dev/null || echo 0)" + + 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 + rc=$? + else + log_info "[$case_name] exec: paplay --device=\"$SINK_NAME\" \"$clip\"" + audio_exec_with_timeout "$TIMEOUT" paplay --device="$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" -lt 0 ] && last_elapsed=0 + + # Evidence + pw_ev=$(audio_evidence_pw_streaming || echo 0) + pa_ev=$(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 + if [ "$rc" -eq 0 ] || { [ "$rc" -eq 124 ] && [ "$dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$min_ok" ]; }; 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) + [ "$AUDIO_BACKEND" = "pulseaudio" ] && pwlog_ev=0 + # Fast teardown fallback: if user-space stream was active, trust ALSA/ASoC too. + if [ "$alsa_ev" -eq 0 ]; then + if [ "$AUDIO_BACKEND" = "pipewire" ] && [ "$pw_ev" -eq 1 ]; then alsa_ev=1; fi + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 1 ]; then alsa_ev=1; fi + fi + if [ "$asoc_ev" -eq 0 ] && [ "$alsa_ev" -eq 1 ]; 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 pw_log=$pwlog_ev" + + if [ "$rc" -eq 0 ]; then + log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s)" + 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)) + 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)) + else + log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s) - see $logf" + fi + + i=$((i + 1)) + done + + if [ "$DMESG_SCAN" -eq 1 ]; then + scan_audio_dmesg "$LOGDIR" + dump_mixers "$LOGDIR/mixer_dump.txt" + fi + + status="FAIL"; [ "$ok_runs" -ge 1 ] && status="PASS" + append_junit "$case_name" "$last_elapsed" "$status" "$logf" + case "$status" in + PASS) pass=$((pass + 1)); echo "$case_name PASS" >> "$LOGDIR/summary.txt" ;; + SKIP) skip=$((skip + 1)); echo "$case_name SKIP" >> "$LOGDIR/summary.txt" ;; + FAIL) fail=$((fail + 1)); echo "$case_name FAIL" >> "$LOGDIR/summary.txt"; suite_rc=1 ;; + esac + done +done + +log_info "Summary: total=$total pass=$pass fail=$fail skip=$skip" + +if [ -n "$JUNIT_OUT" ]; then + tests=$((pass + fail + skip)) + failures="$fail" + skipped="$skip" + { + printf '\n' "$TESTNAME" "$tests" "$failures" "$skipped" + cat "$JUNIT_TMP" + printf '\n' + } > "$JUNIT_OUT" + log_info "Wrote JUnit: $JUNIT_OUT" +fi + +if [ "$suite_rc" -eq 0 ]; then + log_pass "$TESTNAME PASS"; echo "$TESTNAME PASS" > "$RES_FILE" +else + log_fail "$TESTNAME FAIL"; echo "$TESTNAME FAIL" > "$RES_FILE" +fi +exit "$suite_rc" diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md b/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md index e5789cc5..7e6a657d 100644 --- a/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md +++ b/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md @@ -1,41 +1,55 @@ -# Audio encode Validation Script for Qualcomm Linux based platform (Yocto) +# Audio Record Validation Script for Qualcomm Linux-based Platform (Yocto) ## Overview -This script automates the validation of audio encode capabilities on the Qualcomm Linux based platform running a Yocto-based Linux system. It utilizes pulseaudio test app to encode file. +This suite automates the validation of audio recording capabilities on Qualcomm Linux-based platforms running a Yocto-based Linux system. It supports both PipeWire and PulseAudio backends, with robust evidence-based PASS/FAIL logic, asset management, and diagnostic logging. ## Features -- Encode PCM clip with --rate=48000 --format=s16le --channels=1 --file-format=wav /tmp/rec1.wav -d regular0 -- Compatible with Yocto-based root filesystem + +- Supports **PipeWire** and **PulseAudio** backends +- Records audio clips with configurable duration and loop count +- Automatically detects and routes to appropriate source (e.g., mic, null) +- Validates recording using multiple evidence sources: + - PipeWire/PulseAudio streaming state + - ALSA and ASoC runtime status + - Kernel logs (`dmesg`) +- Diagnostic logs: dmesg scan, mixer dumps, playback logs +- Evidence-based validation (user-space, ALSA, ASoC, dmesg) +- Generates `.res` result file and optional JUnit XML output + ## Prerequisites Ensure the following components are present in the target Yocto build: -- `parec` binary(available at /usr/bin) +- PipeWire: `pw-record`, `wpctl` +- PulseAudio: `parecord`, `pactl` +- Common tools: `pgrep`, `timeout`, `grep`, `sed` +- Daemon: `pipewire` or `pulseaudio` must be running + ## Directory Structure ```bash Runner/ -├──suites/ -├ ├── Multimedia/ -│ ├ ├── Audio/ -│ ├ ├ ├── AudioRecord/ -│ ├ ├ ├ ├ └── run.sh -├ ├ ├ ├ ├ └── Read_me.md +├── run-test.sh +├── utils/ +│ ├── functestlib.sh +│ └── audio_common.sh +└── suites/ + └── Multimedia/ + └── Audio/ + ├── AudioRecord/ + ├── run.sh + └── Read_me.md ``` ## Usage - -Instructions - +Instructions: 1. Copy repo to Target Device: Use scp to transfer the scripts from the host to the target device. The scripts should be copied to any directory on the target device. - -2. Verify Transfer: Ensure that the repo have been successfully copied to any directory on the target device. - +2. Verify Transfer: Ensure that the repo has been successfully copied to any directory on the target device. 3. Run Scripts: Navigate to the directory where these files are copied on the target device and execute the scripts as needed. Run a specific test using: @@ -46,34 +60,90 @@ git clone cd scp -r Runner user@target_device_ip: ssh user@target_device_ip -cd Runner && ./run-test.sh AudioRecord + +**Using Unified Runner** +cd Runner +# Run Audiorecord using PipeWire (auto-detects backend if not specified) +./run-test.sh Audiorecord +# Force PulseAudio backend +AUDIO_BACKEND=pulseaudio ./run-test.sh Audiorecord +# Custom options via environment variables +AUDIO_BACKEND=pipewire RECORD_TIMEOUT=20s RECORD_LOOPS=2 RECORD_VOLUME=0.5 ./run-test.sh Audiorecord + +**Directly from Test Directory** +cd Runner/suites/Multimedia/Audio/Audiorecord +# Show usage/help +./run.sh --help +# Run with PipeWire, 3 loops, 10s timeout, mic source +./run.sh --backend pipewire --source mic --loops 3 --timeout 10s +# Run with PulseAudio, null source, strict mode, verbose +./run.sh --backend pulseaudio --source null --strict --verbose + + +Environment Variables: +Variable Description Default +AUDIO_BACKEND Selects backend: pipewire or pulseaudio auto-detect +SOURCE_CHOICE Recording source: mic or null mic +DURATIONS Recording durations: short, medium, long short +RECORD_SECONDS Number of seconds to record (numeric or mapped) 5 +LOOPS Number of recording loops 1 +TIMEOUT Recording timeout per loop (e.g., 15s, 0=none) 0 +STRICT Enable strict mode (fail on any error) 0 +DMESG_SCAN Scan dmesg for errors after recording 1 +VERBOSE Enable verbose logging 0 +JUNIT_OUT Path to write JUnit XML output unset + +CLI Options: +Option Description +--backend Select backend: pipewire or pulseaudio +--source Recording source: mic or null +--durations Recording durations: short, medium, long +--record-seconds Number of seconds to record (numeric or mapped) +--loops Number of recording loops +--timeout Recording timeout per loop (e.g., 15s) +--strict Enable strict mode +--no-dmesg Disable dmesg scan +--junit Write JUnit XML output +--verbose Enable verbose logging +--help Show usage instructions ``` Sample Output: ``` -sh-5.2# cd /Runner/ && ./run-test.sh AudioRecord -[Executing test case: AudioRecord] 2025-05-28 19:03:33 - -[INFO] 2025-05-28 19:03:33 - ------------------------------------------------------------ -[INFO] 2025-05-28 19:03:33 - ------------------- Starting AudioRecord Testcase ------------ -[INFO] 2025-05-28 19:03:33 - Checking if dependency binary is available -[PASS] 2025-05-28 19:03:45 - Recording completed or timed out (ret=124) as expected and output file exists. -[PASS] 2025-05-28 19:03:45 - AudioRecord : Test Passed -[INFO] 2025-05-28 19:03:45 - See results/audiorecord/parec_stdout.log, dmesg_before/after.log, syslog_before/after.log for debug details -[INFO] 2025-05-28 19:03:45 - ------------------- Completed AudioRecord Testcase ------------- -[PASS] 2025-05-28 19:03:45 - AudioRecord passed -sh-5.2# +sh-5.2# ./run.sh --backend pipewire +[INFO] 2025-09-12 06:06:04 - ---------------- Starting AudioRecord ---------------- +[INFO] 2025-09-12 06:06:04 - SoC: 498 +[INFO] 2025-09-12 06:06:04 - Args: backend=pipewire source=mic loops=1 durations='short' rec_secs=30s timeout=0 strict=0 dmesg=1 +[INFO] 2025-09-12 06:06:04 - Using backend: pipewire +[INFO] 2025-09-12 06:06:04 - Routing to source: id/name=48 label='pal source handset mic' choice=mic +[INFO] 2025-09-12 06:06:04 - Watchdog/timeout: 0 +[INFO] 2025-09-12 06:06:04 - [record_short] loop 1/1 start=2025-09-12T06:06:04Z secs=30s backend=pipewire source=mic(48) +[INFO] 2025-09-12 06:06:04 - [record_short] exec: pw-record -v "results/AudioRecord/record_short.wav" +[WARN] 2025-09-12 06:06:34 - [record_short] nonzero rc=124 but recording looks valid (bytes=5738540) - PASS +[INFO] 2025-09-12 06:06:34 - [record_short] evidence: pw_streaming=1 pa_streaming=0 alsa_running=1 asoc_path_on=1 bytes=5738540 pw_log=1 +[PASS] 2025-09-12 06:06:34 - [record_short] loop 1 OK (rc=0, 30s, bytes=5738540) +[INFO] 2025-09-12 06:06:34 - Scanning dmesg for snd|audio|pipewire|pulseaudio: errors & success patterns +[INFO] 2025-09-12 06:06:34 - No snd|audio|pipewire|pulseaudio-related errors found (no OK pattern requested) +[INFO] 2025-09-12 06:06:34 - Summary: total=1 pass=1 fail=0 skip=0 +[PASS] 2025-09-12 06:06:34 - AudioRecord PASS ``` -3. Results will be available in the `Runner/suites/Multimedia/Audio/AudioRecord/AudioRecord.res` directory. +Results: +- Results are stored in: results/Audiorecord/ +- Summary result file: Audiorecord.res +- JUnit XML (if enabled): .xml +- Diagnostic logs: dmesg snapshots, mixer dumps, record logs per test case ## Notes -- The script does not take any arguments. -- It validates the presence of required libraries before executing tests. +- The script validates the presence of required tools before executing tests; missing tools result in SKIP. - If any critical tool is missing, the script exits with an error message. +- Logs include dmesg snapshots, mixer dumps, and record logs. +- Evidence-based PASS/FAIL logic ensures reliability even if backend quirks occur. ## License SPDX-License-Identifier: BSD-3-Clause-Clear (C) Qualcomm Technologies, Inc. and/or its subsidiaries. + diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh index 22c3c4bf..c8ff0d82 100755 --- a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh @@ -1,77 +1,295 @@ #!/bin/sh - # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. # SPDX-License-Identifier: BSD-3-Clause-Clear -# --------- Robustly source init_env and functestlib.sh ---------- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# ---- Source init_env & tools ---- INIT_ENV="" SEARCH="$SCRIPT_DIR" while [ "$SEARCH" != "/" ]; do - if [ -f "$SEARCH/init_env" ]; then - INIT_ENV="$SEARCH/init_env" - break - fi - SEARCH=$(dirname "$SEARCH") + if [ -f "$SEARCH/init_env" ]; then INIT_ENV="$SEARCH/init_env"; break; fi + SEARCH=$(dirname "$SEARCH") done - -if [ -z "$INIT_ENV" ]; then - echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 - exit 1 -fi - -if [ -z "$__INIT_ENV_LOADED" ]; then - # shellcheck disable=SC1090 - . "$INIT_ENV" -fi +[ -z "$INIT_ENV" ] && echo "[ERROR] init_env not found" >&2 && exit 1 +# shellcheck disable=SC1090 +[ -z "$__INIT_ENV_LOADED" ] && . "$INIT_ENV" # shellcheck disable=SC1090,SC1091 . "$TOOLS/functestlib.sh" -# --------------------------------------------------------------- +. "$TOOLS/audio_common.sh" TESTNAME="AudioRecord" -TESTBINARY="parec" -RECORD_FILE="/tmp/rec1.wav" -AUDIO_DEVICE="regular0" -LOGDIR="results/audiorecord" -RESULT_FILE="$TESTNAME.res" - -test_path=$(find_test_case_by_name "$TESTNAME") -cd "$test_path" || exit 1 +RES_FILE="./${TESTNAME}.res" +LOGDIR="results/${TESTNAME}" mkdir -p "$LOGDIR" -chmod -R 777 "$LOGDIR" -log_info "------------------------------------------------------------" -log_info "------------------- Starting $TESTNAME Testcase ------------" +# ---------------- Defaults / CLI ---------------- +AUDIO_BACKEND="" +SRC_CHOICE="${SRC_CHOICE:-mic}" # mic|null +DURATIONS="${DURATIONS:-short}" # label set OR numeric tokens when REC_SECS=auto +REC_SECS="${REC_SECS:-30s}" # DEFAULT: 30s; 'auto' maps short/med/long +LOOPS="${LOOPS:-1}" +TIMEOUT="${TIMEOUT:-0}" # 0 = no watchdog +STRICT="${STRICT:-0}" +DMESG_SCAN="${DMESG_SCAN:-1}" +VERBOSE=0 +JUNIT_OUT="" + +usage() { + cat </dev/null || echo "$SCRIPT_DIR")" +cd "$test_path" || { log_error "cd failed: $test_path"; echo "$TESTNAME FAIL" >"$RES_FILE"; exit 1; } + +log_info "---------------- Starting $TESTNAME ----------------" +log_info "SoC: $(soc_id)" +log_info "Args: backend=${AUDIO_BACKEND:-auto} source=$SRC_CHOICE loops=$LOOPS durations='$DURATIONS' rec_secs=$REC_SECS timeout=$TIMEOUT strict=$STRICT dmesg=$DMESG_SCAN" -log_info "Checking if dependency binary is available" -check_dependencies "$TESTBINARY" pgrep timeout - -# --- Capture logs BEFORE recording (for debugging) --- -dmesg > "$LOGDIR/dmesg_before.log" +# Resolve backend +[ -z "$AUDIO_BACKEND" ] && AUDIO_BACKEND="$(detect_audio_backend)" +if [ -z "$AUDIO_BACKEND" ]; then + log_skip "$TESTNAME SKIP - no audio backend running" + echo "$TESTNAME SKIP" >"$RES_FILE"; exit 0 +fi +log_info "Using backend: $AUDIO_BACKEND" + +if ! check_audio_daemon "$AUDIO_BACKEND"; then + log_skip "$TESTNAME SKIP - backend not available: $AUDIO_BACKEND" + echo "$TESTNAME SKIP" >"$RES_FILE"; exit 0 +fi -# Remove old record file if present -rm -f "$RECORD_FILE" +# Dependencies per backend +if [ "$AUDIO_BACKEND" = "pipewire" ]; then + check_dependencies wpctl pw-record || { log_skip "$TESTNAME SKIP - missing PipeWire utils"; echo "$TESTNAME SKIP" >"$RES_FILE"; exit 0; } +else + check_dependencies pactl parecord || { log_skip "$TESTNAME SKIP - missing PulseAudio utils"; echo "$TESTNAME SKIP" >"$RES_FILE"; exit 0; } +fi -# --- Start recording --- -timeout 12s "$TESTBINARY" --rate=48000 --format=s16le --channels=1 --file-format=wav "$RECORD_FILE" -d "$AUDIO_DEVICE" > "$LOGDIR/parec_stdout.log" 2>&1 -ret=$? +# ----- Route source (set default; recorder uses default source) ----- +SRC_ID="" +case "$AUDIO_BACKEND:$SRC_CHOICE" in + pipewire:null) SRC_ID="$(pw_default_null_source)" ;; + pipewire:*) SRC_ID="$(pw_default_mic)" ;; + pulseaudio:null) SRC_ID="$(pa_default_null_source)" ;; + pulseaudio:*) SRC_ID="$(pa_default_mic)" ;; +esac +if [ -z "$SRC_ID" ]; then + log_skip "$TESTNAME SKIP - requested source '$SRC_CHOICE' not found for $AUDIO_BACKEND" + echo "$TESTNAME SKIP" >"$RES_FILE"; exit 0 +fi -# --- Capture logs AFTER recording (for debugging) --- -dmesg > "$LOGDIR/dmesg_after.log" +if [ "$AUDIO_BACKEND" = "pipewire" ]; then + SRC_LABEL="$(pw_source_label_safe "$SRC_ID")" + wpctl set-default "$SRC_ID" >/dev/null 2>&1 || true + [ -n "$SRC_LABEL" ] || SRC_LABEL="unknown" + log_info "Routing to source: id/name=$SRC_ID label='$SRC_LABEL' choice=$SRC_CHOICE" +else + SRC_LABEL="$(pa_source_name "$SRC_ID" 2>/dev/null || echo "$SRC_ID")" + pa_set_default_source "$SRC_ID" >/dev/null 2>&1 || true + log_info "Routing to source: name='$SRC_LABEL' choice=$SRC_CHOICE" +fi -# --- Evaluate result: pass only if process completed successfully and file is non-empty --- -if ([ "$ret" -eq 0 ] || [ "$ret" -eq 124 ]) && [ -s "$RECORD_FILE" ]; then - log_pass "Recording completed or timed out (ret=$ret) as expected and output file exists." - log_pass "$TESTNAME : Test Passed" - echo "$TESTNAME PASS" > "$RESULT_FILE" - exit 0 +# Watchdog info +dur_s="$(duration_to_secs "$TIMEOUT" 2>/dev/null || echo 0)" +[ -z "$dur_s" ] && dur_s=0 +if [ "$dur_s" -gt 0 ] 2>/dev/null; then + log_info "Watchdog/timeout: ${TIMEOUT}" else - log_fail "parec failed (status $ret) or recorded file missing/empty" - log_fail "$TESTNAME : Test Failed" - echo "$TESTNAME FAIL" > "$RESULT_FILE" - exit 1 + log_info "Watchdog/timeout: disabled (no timeout)" fi -log_info "See $LOGDIR/parec_stdout.log, dmesg_before/after.log, syslog_before/after.log for debug details" -log_info "------------------- Completed $TESTNAME Testcase -------------" -exit 0 +# JUnit init (optional) +if [ -n "$JUNIT_OUT" ]; then JUNIT_TMP="$LOGDIR/.junit_cases.xml"; : > "$JUNIT_TMP"; fi +append_junit() { + name="$1"; elapsed="$2"; status="$3"; logf="$4" + [ -n "$JUNIT_OUT" ] || return 0 + safe_msg="$(tail -n 50 "$logf" 2>/dev/null | sed 's/&/\&/g;s//\>/g')" + { + printf ' \n' "Audio.Record" "$name" "$elapsed" + case "$status" in + PASS) : ;; + SKIP) printf ' \n' ;; + FAIL) printf ' \n' "failed"; printf '%s\n' "$safe_msg"; printf ' \n' ;; + esac + printf ' \n' + } >> "$JUNIT_TMP" +} + +# Auto map if REC_SECS=auto, and accept numeric tokens in DURATIONS like 35s/35sec/35secs/35seconds +auto_secs_for() { + case "$1" in + short) echo "5s" ;; + medium) echo "15s" ;; + long) echo "30s" ;; + *) echo "5s" ;; + esac +} + +# ---------------- Matrix execution ---------------- +total=0; pass=0; fail=0; skip=0; suite_rc=0 + +for dur in $DURATIONS; do + case_name="record_${dur}" + total=$((total + 1)) + logf="$LOGDIR/${case_name}.log"; : > "$logf" + export AUDIO_LOGCTX="$logf" + + secs="$REC_SECS" + if [ "$secs" = "auto" ]; then + tok="$(printf '%s' "$dur" | tr '[:upper:]' '[:lower:]')" + tok_secs="$(printf '%s' "$tok" | sed -n 's/^\([0-9][0-9]*\)\(s\|sec\|secs\|seconds\)$/\1s/p')" + if [ -n "$tok_secs" ]; then + secs="$tok_secs" + else + secs="$(auto_secs_for "$dur")" + fi + fi + + i=1; ok_runs=0; last_elapsed=0 + while [ "$i" -le "$LOOPS" ]; do + iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + effective_timeout="$secs" + if [ -n "$TIMEOUT" ] && [ "$TIMEOUT" != "0" ]; then + effective_timeout="$TIMEOUT" + fi + loop_hdr="source=$SRC_CHOICE" + [ "$AUDIO_BACKEND" = "pipewire" ] && loop_hdr="$loop_hdr($SRC_ID)" || loop_hdr="$loop_hdr($SRC_LABEL)" + log_info "[$case_name] loop $i/$LOOPS start=$iso secs=$secs backend=$AUDIO_BACKEND $loop_hdr" + out="$LOGDIR/${case_name}.wav" + : > "$out" + + start_s="$(date +%s 2>/dev/null || echo 0)" + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + log_info "[$case_name] exec: pw-record -v \"$out\"" + audio_exec_with_timeout "$effective_timeout" pw-record -v "$out" >>"$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + + 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 + + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + log_warn "[$case_name] first attempt rc=$rc bytes=$bytes; retry with --target $SRC_ID" + : > "$out" + log_info "[$case_name] exec: pw-record -v --target \"$SRC_ID\" \"$out\"" + audio_exec_with_timeout "$effective_timeout" pw-record -v --target "$SRC_ID" "$out" >>"$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_warn "[$case_name] nonzero rc=$rc after retry but recording looks valid (bytes=$bytes) - PASS" + rc=0 + fi + fi + else + log_info "[$case_name] exec: parecord --file-format=wav \"$out\"" + audio_exec_with_timeout "$effective_timeout" parecord --file-format=wav "$out" >>"$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + 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 + end_s="$(date +%s 2>/dev/null || echo 0)" + last_elapsed=$((end_s - start_s)); [ "$last_elapsed" -lt 0 ] && last_elapsed=0 + + # Evidence + pw_ev=$(audio_evidence_pw_streaming || echo 0) + pa_ev=$(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 + if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; 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) + [ "$AUDIO_BACKEND" = "pulseaudio" ] && pwlog_ev=0 + # Fast teardown fallback: if user-space stream was active, trust ALSA/ASoC too. + if [ "$alsa_ev" -eq 0 ]; then + if [ "$AUDIO_BACKEND" = "pipewire" ] && [ "$pw_ev" -eq 1 ]; then alsa_ev=1; fi + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 1 ]; then alsa_ev=1; fi + fi + if [ "$asoc_ev" -eq 0 ] && [ "$alsa_ev" -eq 1 ]; 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" + + # Final PASS/FAIL + 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)" + ok_runs=$((ok_runs + 1)) + else + log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}) - see $logf" + fi + + i=$((i + 1)) + done + + if [ "$DMESG_SCAN" -eq 1 ]; then + scan_audio_dmesg "$LOGDIR" + dump_mixers "$LOGDIR/mixer_dump.txt" + fi + + status="FAIL"; [ "$ok_runs" -ge 1 ] && status="PASS" + append_junit "$case_name" "$last_elapsed" "$status" "$logf" + case "$status" in + PASS) pass=$((pass + 1)); echo "$case_name PASS" >> "$LOGDIR/summary.txt" ;; + SKIP) skip=$((skip + 1)); echo "$case_name SKIP" >> "$LOGDIR/summary.txt" ;; + FAIL) fail=$((fail + 1)); echo "$case_name FAIL" >> "$LOGDIR/summary.txt"; suite_rc=1 ;; + esac +done + +log_info "Summary: total=$total pass=$pass fail=$fail skip=$skip" + +if [ -n "$JUNIT_OUT" ]; then + tests=$((pass + fail + skip)) + failures="$fail" + skipped="$skip" + { + printf '\n' "$TESTNAME" "$tests" "$failures" "$skipped" + cat "$JUNIT_TMP" + printf '\n' + } > "$JUNIT_OUT" + log_info "Wrote JUnit: $JUNIT_OUT" +fi + +if [ "$suite_rc" -eq 0 ]; then + log_pass "$TESTNAME PASS"; echo "$TESTNAME PASS" > "$RES_FILE" +else + log_fail "$TESTNAME FAIL"; echo "$TESTNAME FAIL" > "$RES_FILE" +fi +exit "$suite_rc" diff --git a/Runner/utils/audio_common.sh b/Runner/utils/audio_common.sh new file mode 100755 index 00000000..815f0d6c --- /dev/null +++ b/Runner/utils/audio_common.sh @@ -0,0 +1,549 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# Common audio helpers for PipeWire / PulseAudio runners. +# Requires: functestlib.sh (log_* helpers, extract_tar_from_url, scan_dmesg_errors) + +# ---------- Backend detection & daemon checks ---------- +detect_audio_backend() { + if pgrep -x pipewire >/dev/null 2>&1 && command -v wpctl >/dev/null 2>&1; then + echo pipewire; return 0 + fi + if pgrep -x pulseaudio >/dev/null 2>&1 && command -v pactl >/dev/null 2>&1; then + echo pulseaudio; return 0 + fi + # Accept pipewire-pulse shim as PulseAudio + if pgrep -x pipewire-pulse >/dev/null 2>&1 && command -v pactl >/dev/null 2>&1; then + echo pulseaudio; return 0 + fi + echo "" + return 1 +} + +check_audio_daemon() { + case "$1" in + pipewire) pgrep -x pipewire >/dev/null 2>&1 ;; + pulseaudio) pgrep -x pulseaudio >/dev/null 2>&1 || pgrep -x pipewire-pulse >/dev/null 2>&1 ;; + *) return 1 ;; + esac +} + +# ---------- Assets / clips ---------- +resolve_clip() { + fmt="$1"; dur="$2"; base="AudioClips" + case "$fmt:$dur" in + wav:short|wav:medium|wav:long) printf '%s\n' "$base/yesterday_48KHz.wav" ;; + *) printf '%s\n' "" ;; + esac +} + +ensure_playback_clip() { + clip="$1" + [ -f "$clip" ] && return 0 + if [ -n "$AUDIO_TAR_URL" ]; then + log_info "Audio clip missing, extracting from $AUDIO_TAR_URL" + extract_tar_from_url "$AUDIO_TAR_URL" || return 1 + fi + [ -f "$clip" ] +} + +# ---------- dmesg + mixer dumps ---------- +scan_audio_dmesg() { + outdir="$1"; mods='snd|audio|pipewire|pulseaudio'; excl='dummy regulator|EEXIST|probe deferred' + scan_dmesg_errors "$mods" "$outdir" "$excl" || true +} + +dump_mixers() { + out="$1" + { + echo "---- wpctl status ----" + command -v wpctl >/dev/null 2>&1 && wpctl status 2>&1 || echo "(wpctl not found)" + echo "---- pactl list ----" + command -v pactl >/dev/null 2>&1 && pactl list 2>&1 || echo "(pactl not found)" + } >"$out" 2>/dev/null +} + +# Returns child exit code (124 when killed by timeout). If tmo<=0, runs the +# command directly (no watchdog). + +# ---------- Timeout runner (prefers provided wrappers) ---------- +# Returns child's exit code. For the fallback-kill path, returns 143 on timeout. +audio_timeout_run() { + tmo="$1"; shift + + # 0/empty => run without a watchdog (do NOT background/kill) + case "$tmo" in ""|0|"0s"|"0S") "$@"; return $? ;; esac + + # Use project-provided wrappers if available + if command -v run_with_timeout >/dev/null 2>&1; then + run_with_timeout "$tmo" "$@"; return $? + fi + if command -v sh_timeout >/dev/null 2>&1; then + sh_timeout "$tmo" "$@"; return $? + fi + if command -v timeout >/dev/null 2>&1; then + timeout "$tmo" "$@"; return $? + fi + + # Last-resort busybox-safe watchdog + # Normalize "15s" -> 15 + sec="$(printf '%s' "$tmo" | sed 's/[sS]$//')" + [ -z "$sec" ] && sec="$tmo" + # If parsing failed for some reason, just run directly + case "$sec" in ''|*[!0-9]* ) "$@"; return $? ;; esac + + "$@" & + pid=$! + t=0 + while kill -0 "$pid" 2>/dev/null; do + if [ "$t" -ge "$sec" ]; then + kill "$pid" 2>/dev/null + wait "$pid" 2>/dev/null + return 143 + fi + sleep 1; t=$((t+1)) + done + wait "$pid"; return $? +} + +# ---------- PipeWire: sinks (playback) ---------- +pw_default_speakers() { + _block="$(wpctl status 2>/dev/null | sed -n '/Sinks:/,/Sources:/p')" + _id="$(printf '%s\n' "$_block" \ + | grep -i -E 'speaker|headphone' \ + | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ + | head -n1)" + [ -n "$_id" ] || _id="$(printf '%s\n' "$_block" \ + | sed -n 's/^[^*]*\*[[:space:]]*\([0-9][0-9]*\)\..*/\1/p' \ + | head -n1)" + [ -n "$_id" ] || _id="$(printf '%s\n' "$_block" \ + | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ + | head -n1)" + printf '%s\n' "$_id" +} + +pw_default_null() { + wpctl status 2>/dev/null \ + | sed -n '/Sinks:/,/Sources:/p' \ + | grep -i -E 'null|dummy|loopback|monitor' \ + | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ + | head -n1 +} + +pw_sink_name_safe() { + id="$1"; [ -n "$id" ] || { echo ""; return 1; } + name="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.description' | cut -d'"' -f2)" + [ -n "$name" ] || name="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.name' | cut -d'"' -f2)" + if [ -z "$name" ]; then + name="$(wpctl status 2>/dev/null \ + | sed -n '/Sinks:/,/Sources:/p' \ + | grep -E "^[^0-9]*${id}[.][[:space:]]" \ + | sed 's/^[^0-9]*[0-9]\+[.][[:space:]]\+//' \ + | sed 's/[[:space:]]*\[vol:.*$//' \ + | head -n1)" + fi + printf '%s\n' "$name" +} + +pw_sink_name() { pw_sink_name_safe "$@"; } # back-compat alias +pw_set_default_sink() { [ -n "$1" ] && wpctl set-default "$1" >/dev/null 2>&1; } + +# ---------- PipeWire: sources (record) ---------- +pw_default_mic() { + blk="$(wpctl status 2>/dev/null | sed -n '/Sources:/,/^$/p')" + id="$(printf '%s\n' "$blk" | grep -i 'mic' | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' | head -n1)" + [ -n "$id" ] || id="$(printf '%s\n' "$blk" | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' | head -n1)" + printf '%s\n' "$id" +} + +pw_default_null_source() { + blk="$(wpctl status 2>/dev/null | sed -n '/Sources:/,/^$/p')" + id="$(printf '%s\n' "$blk" | grep -i 'null\|dummy' | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' | head -n1)" + printf '%s\n' "$id" +} + +pw_set_default_source() { [ -n "$1" ] && wpctl set-default "$1" >/dev/null 2>&1; } + +pw_source_label_safe() { + id="$1"; [ -n "$id" ] || { echo ""; return 1; } + label="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.description' | cut -d'"' -f2)" + [ -n "$label" ] || label="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.name' | cut -d'"' -f2)" + if [ -z "$label" ]; then + label="$(wpctl status 2>/dev/null \ + | sed -n '/Sources:/,/Filters:/p' \ + | grep -E "^[^0-9]*${id}[.][[:space:]]" \ + | sed 's/^[^0-9]*[0-9]\+[.][[:space:]]\+//' \ + | sed 's/[[:space:]]*\[vol:.*$//' \ + | head -n1)" + fi + printf '%s\n' "$label" +} + +# ---------- PulseAudio: sinks (playback) ---------- +pa_default_speakers() { + def="$(pactl info 2>/dev/null | sed -n 's/^Default Sink:[[:space:]]*//p' | head -n1)" + if [ -n "$def" ]; then printf '%s\n' "$def"; return 0; fi + name="$(pactl list short sinks 2>/dev/null | awk '{print $2}' | grep -i 'speaker\|head' | head -n1)" + [ -n "$name" ] || name="$(pactl list short sinks 2>/dev/null | awk '{print $2}' | head -n1)" + printf '%s\n' "$name" +} + +pa_default_null() { + pactl list short sinks 2>/dev/null | awk '{print $2}' | grep -i 'null\|dummy' | head -n1 +} + +pa_set_default_sink() { [ -n "$1" ] && pactl set-default-sink "$1" >/dev/null 2>&1; } + +# Map numeric index → sink name; pass through names unchanged +pa_sink_name() { + id="$1" + case "$id" in + '' ) echo ""; return 0;; + *[!0-9]* ) echo "$id"; return 0;; + * ) pactl list short sinks 2>/dev/null | awk -v k="$id" '$1==k{print $2; exit}'; return 0;; + esac +} + +# ---------- PulseAudio: sources (record) ---------- +pa_default_source() { + s="$(pactl get-default-source 2>/dev/null | tr -d '\r')" + [ -n "$s" ] || s="$(pactl info 2>/dev/null | awk -F': ' '/Default Source:/{print $2}')" + [ -n "$s" ] || s="$(pactl list short sources 2>/dev/null | awk 'NR==1{print $2}')" + printf '%s\n' "$s" +} + +pa_set_default_source() { [ -n "$1" ] && pactl set-default-source "$1" >/dev/null 2>&1 || true; } + +pa_source_name() { + id="$1"; [ -n "$id" ] || return 1 + if pactl list short sources 2>/dev/null | awk '{print $1}' | grep -qx "$id"; then + pactl list short sources 2>/dev/null | awk -v idx="$id" '$1==idx{print $2; exit}' + else + printf '%s\n' "$id" + fi +} + +pa_resolve_mic_fallback() { + s="$(pactl list short sources 2>/dev/null \ + | awk 'BEGIN{IGNORECASE=1} /mic|handset|headset|speaker_mic|voice/ {print $2; exit}')" + [ -n "$s" ] || s="$(pactl list short sources 2>/dev/null | awk 'NR==1{print $2}')" + printf '%s\n' "$s" +} + +# PipeWire sink label by ID (tries description, then node.name, then status line) +pw_sink_name_safe() { + id="$1"; [ -n "$id" ] || return 1 + name="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.description' | cut -d'"' -f2)" + [ -n "$name" ] || name="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.name' | cut -d'"' -f2)" + if [ -z "$name" ]; then + name="$(wpctl status 2>/dev/null \ + | sed -n '/^[[:space:]]*Sinks:/,/^[[:space:]]*$/p' \ + | grep -E "^[[:space:]]*\*?[[:space:]]*${id}[.][[:space:]]" \ + | sed 's/^[[:space:]]*\*\?[[:space:]]*[0-9]\+[.][[:space:]]\+//' \ + | sed 's/[[:space:]]*\[vol:.*$//' \ + | head -n1)" + fi + printf '%s\n' "$name" +} + +# ----------- PulseAudio Source Helpers ----------- +pa_default_mic() { + def="$(pactl info 2>/dev/null | sed -n 's/^Default Source:[[:space:]]*//p' | head -n1)" + if [ -n "$def" ]; then + printf '%s\n' "$def"; return 0 + fi + name="$(pactl list short sources 2>/dev/null | awk '{print $2}' | grep -i 'mic' | head -n1)" + [ -n "$name" ] || name="$(pactl list short sources 2>/dev/null | awk '{print $2}' | head -n1)" + printf '%s\n' "$name" +} +pa_default_null_source() { + name="$(pactl list short sources 2>/dev/null | awk '{print $2}' | grep -i 'null\|dummy' | head -n1)" + printf '%s\n' "$name" +} + + +# ---------- Evidence helpers (used by run.sh for PASS-on-evidence) ---------- +# PipeWire: 1 if any output audio stream exists; fallback parses Streams: block +audio_evidence_pw_streaming() { + # Try wpctl (fast); fall back to log scan if AUDIO_LOGCTX is available + if command -v wpctl >/dev/null 2>&1; then + # Count Input/Output streams in RUNNING state + wpctl status 2>/dev/null | grep -Eq 'RUNNING' && { echo 1; return; } + fi + # Fallback to log + if [ -n "${AUDIO_LOGCTX:-}" ] && [ -r "$AUDIO_LOGCTX" ]; then + grep -qiE 'paused -> streaming|stream time:' "$AUDIO_LOGCTX" 2>/dev/null && { echo 1; return; } + fi + echo 0 +} + +# 2) PulseAudio streaming - safe when PA is absent (returns 0 without forcing FAIL) +#Return 1 if PulseAudio is actively streaming (sink-inputs, source-outputs, or RUNNING sink), +# else 0. Works even when the PA daemon is a different user by trying sockets + cookies. +audio_evidence_pa_streaming() { + # quick exits if tools are missing + command -v pactl >/dev/null 2>&1 || command -v pacmd >/dev/null 2>&1 || { + # final fallback: try to infer from our log if present + if [ -n "${AUDIO_LOGCTX:-}" ] && [ -s "$AUDIO_LOGCTX" ]; then + grep -qiE 'Connected to PulseAudio|Opening audio stream|Stream started|Starting recording|Playing' "$AUDIO_LOGCTX" && { echo 1; return; } + fi + echo 0; return + } + + # build candidate socket + cookie pairs + cand="" + # per-user runtime dir sockets + for d in /run/user/* /var/run/user/*; do + [ -S "$d/pulse/native" ] || continue + sock="$d/pulse/native" + cookie="" + [ -r "$d/pulse/cookie" ] && cookie="$d/pulse/cookie" + # try to derive a home cookie for that uid as well + uid="$(stat -c %u "$d" 2>/dev/null || echo)" + if [ -n "$uid" ]; then + home="$(getent passwd "$uid" 2>/dev/null | awk -F: '{print $6}')" + [ -n "$home" ] && [ -r "$home/.config/pulse/cookie" ] && cookie="$home/.config/pulse/cookie" + fi + cand="$cand|$sock|$cookie" + done + # system-wide socket (no per-user cookie nearby) + for s in /run/pulse/native /var/run/pulse/native; do + [ -S "$s" ] && cand="$cand|$s|" + done + # also try current env (no explicit socket) + cand="$cand|::env::|" + + # try pactl first with cookie if available + if command -v pactl >/dev/null 2>&1; then + IFS='|' read -r _ sock cookie rest </dev/null 2>&1 || true + if pactl list sinks 2>/dev/null | grep -qi -m1 '^[[:space:]]*State:[[:space:]]*RUNNING' \ + || pactl list short sink-inputs 2>/dev/null | grep -q '^[0-9]\+' \ + || pactl list short source-outputs 2>/dev/null | grep -q '^[0-9]\+' ; then + echo 1; return + fi + else + if [ -n "$cookie" ]; then + PULSE_SERVER="unix:$sock" PULSE_COOKIE="$cookie" pactl info >/dev/null 2>&1 || { IFS='|' read -r sock cookie rest </dev/null | grep -qi -m1 '^[[:space:]]*State:[[:space:]]*RUNNING' \ + || PULSE_SERVER="unix:$sock" PULSE_COOKIE="$cookie" pactl list short sink-inputs 2>/dev/null | grep -q '^[0-9]\+' \ + || PULSE_SERVER="unix:$sock" PULSE_COOKIE="$cookie" pactl list short source-outputs 2>/dev/null | grep -q '^[0-9]\+' ; then + echo 1; return + fi + else + PULSE_SERVER="unix:$sock" pactl info >/dev/null 2>&1 || { IFS='|' read -r sock cookie rest </dev/null | grep -qi -m1 '^[[:space:]]*State:[[:space:]]*RUNNING' \ + || PULSE_SERVER="unix:$sock" pactl list short sink-inputs 2>/dev/null | grep -q '^[0-9]\+' \ + || PULSE_SERVER="unix:$sock" pactl list short source-outputs 2>/dev/null | grep -q '^[0-9]\+' ; then + echo 1; return + fi + fi + fi + IFS='|' read -r sock cookie rest </dev/null 2>&1; then + IFS='|' read -r _ sock cookie rest </dev/null 2>&1 || true + if pacmd list-sinks 2>/dev/null | grep -qi -m1 '^[[:space:]]*state:[[:space:]]*RUNNING' \ + || pacmd list-sink-inputs 2>/dev/null | grep -q -m1 '^[[:space:]]*index:' \ + || pacmd list-source-outputs 2>/dev/null | grep -q -m1 '^[[:space:]]*index:' ; then + echo 1; return + fi + else + # pacmd -s doesn't use PULSE_COOKIE directly, but trying -s is still useful when the server is accessible + pacmd -s "unix:$sock" stat >/dev/null 2>&1 || { IFS='|' read -r sock cookie rest </dev/null | grep -qi -m1 '^[[:space:]]*state:[[:space:]]*RUNNING' \ + || pacmd -s "unix:$sock" list-sink-inputs 2>/dev/null | grep -q -m1 '^[[:space:]]*index:' \ + || pacmd -s "unix:$sock" list-source-outputs 2>/dev/null | grep -q -m1 '^[[:space:]]*index:' ; then + echo 1; return + fi + fi + IFS='|' read -r sock cookie rest </dev/null; then + echo 1; return + fi + + # Many QCS boards expose lots of Playback/Capture endpoints; if any of them say "On", mark active + dapm_pc_files="$(grep -RIl --binary-files=text -E '/dapm/.*(Playback|Capture)$' "$base"/*/dapm 2>/dev/null)" + if [ -n "$dapm_pc_files" ]; then + echo "$dapm_pc_files" | xargs -r grep -I -q -E ':\s*On(\s|$)' 2>/dev/null && { echo 1; return; } + fi + + # Some kernels only flip bias level when any path is active + if grep -RIlq --binary-files=text '/dapm/bias_level$' "$base"/*/dapm 2>/dev/null; then + grep -RIl --binary-files=text '/dapm/bias_level$' "$base"/*/dapm 2>/dev/null \ + | xargs -r grep -I -q -E 'On|Standby' 2>/dev/null && { echo 1; return; } + fi + + # Fallback heuristic: if ALSA says a PCM substream is RUNNING, assume DAPM is up + if audio_evidence_alsa_running_any 2>/dev/null | grep -qx 1; then + echo 1; return + fi + + echo 0 +} +# 5) PW log evidence (optional, from AUDIO_LOGCTX) +audio_evidence_pw_log_seen() { + if [ -n "${AUDIO_LOGCTX:-}" ] && [ -r "$AUDIO_LOGCTX" ]; then + grep -qiE 'paused -> streaming|stream time:' "$AUDIO_LOGCTX" 2>/dev/null && { echo 1; return; } + fi + echo 0 +} + + +# Parse a human duration into integer seconds. +# Prints seconds to stdout on success, returns 0. +# Prints nothing and returns non-zero on failure. +# +# Accepted examples: +# "15" "15s" "15sec" "15secs" "15second" "15seconds" +# "2m" "2min" "2mins" "2minute" "2minutes" +# "1h" "1hr" "1hrs" "1hour" "1hours" +# "1h30m" "2m10s" "1h2m3s" (any combination h/m/s) +# "90s" "120m" "3h" +# "MM:SS" (e.g., "01:30" -> 90) +# "HH:MM:SS" (e.g., "2:03:04" -> 7384) +audio_parse_secs() { + in="$*" + norm=$(printf '%s' "$in" | tr -d ' \t\r\n' | tr '[:upper:]' '[:lower:]') + [ -n "$norm" ] || return 1 + + case "$norm" in + *:*) + IFS=':' set -- "$norm" + for p in "$@"; do case "$p" in ''|*[!0-9]*) return 1;; esac; done + case $# in + 2) h=0; m=$1; s=$2 ;; + 3) h=$1; m=$2; s=$3 ;; + *) return 1 ;; + esac + printf '%s\n' $(( ${h:-0}*3600 + ${m:-0}*60 + ${s:-0} )) + return 0 + ;; + *[!0-9]*) + case "$norm" in + [0-9]*s|[0-9]*sec|[0-9]*secs|[0-9]*second|[0-9]*seconds) + n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' "$n"; return 0 ;; + [0-9]*m|[0-9]*min|[0-9]*mins|[0-9]*minute|[0-9]*minutes) + n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' $((n*60)); return 0 ;; + [0-9]*h|[0-9]*hr|[0-9]*hrs|[0-9]*hour|[0-9]*hours) + n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' $((n*3600)); return 0 ;; + *) + tokens=$(printf '%s' "$norm" | sed 's/\([0-9][0-9]*[a-z][a-z]*\)/\1 /g') + total=0; ok=0 + for t in $tokens; do + n=$(printf '%s' "$t" | sed -n 's/^\([0-9][0-9]*\).*/\1/p') || return 1 + u=$(printf '%s' "$t" | sed -n 's/^[0-9][0-9]*\([a-z][a-z]*\)$/\1/p') + case "$u" in + s|sec|secs|second|seconds) add=$n ;; + m|min|mins|minute|minutes) add=$((n*60)) ;; + h|hr|hrs|hour|hours) add=$((n*3600)) ;; + *) return 1 ;; + esac + total=$((total+add)); ok=1 + done + [ "$ok" -eq 1 ] 2>/dev/null || return 1 + printf '%s\n' "$total" + return 0 + ;; + esac + ;; + *) + printf '%s\n' "$norm" + return 0 + ;; + esac + return 1 +} + +# --- Local watchdog that always honors the first argument (e.g. "15" or "15s") --- +audio_exec_with_timeout() { + dur="$1"; shift + # normalize: allow "15" or "15s" + case "$dur" in + ""|"0") dur_norm=0 ;; + *s) dur_norm="${dur%s}" ;; + *) dur_norm="$dur" ;; + esac + + # numeric? if not, treat as no-timeout + case "$dur_norm" in *[!0-9]*|"") dur_norm=0 ;; esac + + if [ "$dur_norm" -gt 0 ] 2>/dev/null && command -v timeout >/dev/null 2>&1; then + timeout "$dur_norm" "$@"; return $? + fi + + if [ "$dur_norm" -gt 0 ] 2>/dev/null; then + # portable fallback watchdog + "$@" & + pid=$! + ( + sleep "$dur_norm" + kill -TERM "$pid" 2>/dev/null || true + sleep 1 + kill -KILL "$pid" 2>/dev/null || true + ) & + w=$! + wait "$pid"; rc=$? + kill -TERM "$w" 2>/dev/null || true + # map "killed by watchdog" to 124 (GNU timeout convention) + [ "$rc" -eq 143 ] && rc=124 + return "$rc" + fi + + # no timeout + "$@" +} + diff --git a/Runner/utils/functestlib.sh b/Runner/utils/functestlib.sh index 13fea02e..ae6c690a 100755 --- a/Runner/utils/functestlib.sh +++ b/Runner/utils/functestlib.sh @@ -261,13 +261,21 @@ detect_runner_root() { # Function is to check for network connectivity status check_network_status() { echo "[INFO] Checking network connectivity..." - - # Get first active IPv4 address (excluding loopback) - ip_addr=$(ip -4 addr show scope global up | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -n 1) - + + # Prefer the egress/source IP chosen by the routing table (most accurate). + ip_addr=$(ip -4 route get 1.1.1.1 2>/dev/null \ + | awk 'NR==1{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}') + + # Fallback: first global IPv4 on any UP interface (works even without a default route). + if [ -z "$ip_addr" ]; then + ip_addr=$(ip -o -4 addr show scope global up 2>/dev/null \ + | awk 'NR==1{split($4,a,"/"); print a[1]}') + fi + if [ -n "$ip_addr" ]; then echo "[PASS] Network is active. IP address: $ip_addr" - + + # Quick reachability probe (single ICMP). BusyBox-compatible flags. if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then echo "[PASS] Internet is reachable." return 0 @@ -1962,6 +1970,7 @@ configure_pipeline_block() { media-ctl -d "$MEDIA_NODE" -V "$vline_fallback" || true fi fi + done # 2) Apply links exactly as parsed (same order as your manual sequence). @@ -2229,3 +2238,53 @@ print_path_meta() { # shellcheck disable=SC2012 ls -ld -- "$p" 2>/dev/null } + +soc_id() { + if [ -r /sys/devices/soc0/soc_id ]; then + cat /sys/devices/soc0/soc_id 2>/dev/null + elif [ -r /proc/device-tree/compatible ]; then + tr -d '\0' /dev/null | head -n 1 + else + uname -r + fi +} + +# --- BusyBox-safe helper: convert "15s" -> 15, pass-through if already integer --- +timeout_to_seconds() { + case "$1" in *s) echo "${1%s}";; *) echo "$1";; esac +} + +# --- POSIX timeout without GNU coreutils --- +sh_timeout() { + dur="$1"; shift + [ "$1" = "--" ] && shift + case "$dur" in *s) sec=${dur%s} ;; *) sec=$dur ;; esac + [ -z "$sec" ] && sec=0 + + "$@" & + pid=$! + + t=0 + while kill -0 "$pid" 2>/dev/null; do + if [ "$t" -ge "$sec" ] 2>/dev/null; then + kill "$pid" 2>/dev/null || true + sleep 1 + kill -9 "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null + return 124 + fi + sleep 1 + t=$((t+1)) + done + wait "$pid"; return $? +} + +# Map test duration keywords to seconds +duration_to_secs() { + case "$1" in + short) echo 5 ;; + medium) echo 15 ;; + long) echo 30 ;; + *) echo 5 ;; + esac +} \ No newline at end of file