Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Tests

on:
push:
branches: [dev]
pull_request:
branches: [dev]
workflow_dispatch:

jobs:
bats:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- name: Install bats
run: sudo apt-get update -qq && sudo apt-get install -y -qq bats

- name: Run tests
run: bats test/
226 changes: 226 additions & 0 deletions test/audio-utils.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#!/usr/bin/env bats
# Unit tests for lib/audio-utils.sh
# The lib's external commands (aplay, wpctl, pactl, pgrep...) are replaced by
# PATH-prepended stubs in test/stubs, fed from canned output in test/fixtures.

setup() {
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
FIXTURES="$REPO_ROOT/test/fixtures"
STUBS="$REPO_ROOT/test/stubs"
PATH="$STUBS:$PATH"

export WPCTL_FIXTURE_DIR="$FIXTURES"
export WPCTL_STATUS_FIXTURE="$FIXTURES/wpctl_status.txt"
export PACTL_SINKS_FIXTURE="$FIXTURES/pactl_list_sinks.txt"
export PACTL_SHORT_SINKS_FIXTURE="$FIXTURES/pactl_short_sinks.txt"

# Keep the lib's logging out of /tmp during tests
export LOG_FILE="$BATS_TEST_TMPDIR/test.log"

# shellcheck source=../lib/audio-utils.sh
source "$REPO_ROOT/lib/audio-utils.sh"
}

# ---------------------------------------------------------------- aplay parsing

@test "get_card_number finds the bcm2835 headphones card" {
export APLAY_FIXTURE="$FIXTURES/aplay_all_cards.txt"
run get_card_number "bcm2835"
[ "$status" -eq 0 ]
[ "$output" == "1" ]
}

@test "get_card_number returns empty for an absent card" {
export APLAY_FIXTURE="$FIXTURES/aplay_all_cards.txt"
run get_card_number "snd_rpi_proto"
[ "$status" -eq 0 ]
[ -z "$output" ]
}

@test "pick_usb_card returns the single USB card" {
export APLAY_FIXTURE="$FIXTURES/aplay_all_cards.txt"
run pick_usb_card
[ "$status" -eq 0 ]
[ "$output" == "3" ]
}

@test "pick_usb_card picks the LAST card and warns when several USB cards exist" {
export APLAY_FIXTURE="$FIXTURES/aplay_two_usb.txt"
run pick_usb_card
[ "$status" -eq 0 ]
[[ "$output" == *"Multiple USB soundcards detected"* ]]
[ "${lines[-1]}" == "2" ]
}

@test "pick_usb_card returns empty when no USB card exists" {
export APLAY_FIXTURE="$FIXTURES/aplay_onboard_only.txt"
run pick_usb_card
[ "$status" -eq 0 ]
[ -z "$output" ]
}

# ------------------------------------------- fallback priority (USB > HAT > headphones > HDMI)

@test "priority: USB wins over HAT, headphones and HDMI" {
export APLAY_FIXTURE="$FIXTURES/aplay_all_cards.txt"
run select_fallback_card
[ "$output" == "usb 3" ]
}

@test "priority: HAT (user-installed card) wins when no USB is present" {
export APLAY_FIXTURE="$FIXTURES/aplay_hat_only.txt"
run select_fallback_card
[ "$output" == "other 2" ]
}

@test "priority: headphones (bcm2835) win when only onboard cards are present" {
export APLAY_FIXTURE="$FIXTURES/aplay_onboard_only.txt"
run select_fallback_card
[ "$output" == "headphones 1" ]
}

@test "priority: HDMI is the last resort" {
export APLAY_FIXTURE="$FIXTURES/aplay_hdmi_only.txt"
run select_fallback_card
[ "$output" == "hdmi 0" ]
}

@test "priority: empty result when no card at all is detected" {
export APLAY_FIXTURE="$FIXTURES/aplay_none.txt"
run select_fallback_card
[ "$status" -eq 0 ]
[ -z "$output" ]
}

# ---------------------------------------------------------------- wpctl parsing

@test "pw_list_sink_ids parses sink IDs from wpctl status (incl. the default sink)" {
run pw_list_sink_ids
[ "$status" -eq 0 ]
[ "${lines[0]}" == "50" ]
[ "${lines[1]}" == "55" ]
[ "${#lines[@]}" -eq 2 ]
}

@test "pw_card2sink maps an ALSA card index to the PipeWire sink ID" {
run pw_card2sink 3
[ "$status" -eq 0 ]
[ "$output" == "55" ]
}

@test "pw_card2sink returns nothing for an unknown card index" {
run pw_card2sink 9
[ -z "$output" ]
}

@test "pw_sink_by_name finds a sink by node.name" {
run pw_sink_by_name "alsa_output.usb-0d8c_USB_Audio_Device-00.analog-stereo"
[ "$status" -eq 0 ]
[ "$output" == "55" ]
}

@test "pw_sink_by_name fails (rc 1) for an unknown node.name" {
run pw_sink_by_name "auto_combined"
[ "$status" -eq 1 ]
}

# ---------------------------------------------------------------- pactl parsing

@test "pulse_card2sink maps an ALSA card index to the sink name" {
run pulse_card2sink 3
[ "$status" -eq 0 ]
[ "$output" == "alsa_output.usb-0d8c_USB_Audio_Device-00.analog-stereo" ]
}

@test "pulse_card2sink reports 'Card index not found' for an unknown index" {
run pulse_card2sink 9
[ "$status" -eq 0 ]
[ "$output" == "Card index not found" ]
}

@test "pulse_list_sinks excludes auto_null and auto_combined" {
run pulse_list_sinks
[ "$status" -eq 0 ]
[ "${#lines[@]}" -eq 2 ]
[ "${lines[0]}" == "alsa_output.platform-bcm2835_audio.stereo-fallback" ]
[ "${lines[1]}" == "alsa_output.usb-0d8c_USB_Audio_Device-00.analog-stereo" ]
}

# ---------------------------------------------------------------- udev env vars

@test "udev_usb_card extracts the card number from DEVPATH" {
DEVPATH="/devices/platform/soc/usb/1-1.2:1.0/sound/card3" run udev_usb_card
[ "$output" == "3" ]
}

@test "udev_usb_card extracts the card number from DEVNAME" {
DEVNAME="/dev/snd/controlC5" run udev_usb_card
[ "$output" == "5" ]
}

@test "udev_usb_card returns empty outside of udev" {
run udev_usb_card
[ "$status" -eq 0 ]
[ -z "$output" ]
}

# ---------------------------------------------------------------- i2c hint

@test "read_i2c_platform returns the hint written by ovos-i2csound" {
I2C_PLATFORM_FILE="$BATS_TEST_TMPDIR/i2c_platform"
echo "MARK1" > "$I2C_PLATFORM_FILE"
run read_i2c_platform
[ "$output" == "MARK1" ]
}

@test "read_i2c_platform returns empty when the hint file is missing" {
I2C_PLATFORM_FILE="$BATS_TEST_TMPDIR/does_not_exist"
run read_i2c_platform
[ "$status" -eq 0 ]
[ -z "$output" ]
}

# ---------------------------------------------------------------- sound server detection

# detect_sound_server probes with pgrep and command -v; run it in a controlled
# PATH containing only the binaries each scenario should "have".
_detect_with() { # $1: csv of fake binaries to provide
local dir="$BATS_TEST_TMPDIR/bin_$BATS_TEST_NUMBER"
mkdir -p "$dir"
local IFS=','
for b in $1; do
printf '#!/bin/sh\nexit 1\n' > "$dir/$b" # pgrep stub: nothing running
chmod +x "$dir/$b"
done
PATH="$dir" run detect_sound_server
}

@test "detect_sound_server: running pipewire wins" {
local dir="$BATS_TEST_TMPDIR/bin_pw"
mkdir -p "$dir"
printf '#!/bin/sh\n[ "$2" = "pipewire" ] && exit 0 || exit 1\n' > "$dir/pgrep"
chmod +x "$dir/pgrep"
PATH="$dir" run detect_sound_server
[ "$output" == "pipewire" ]
}

@test "detect_sound_server: installed pipewire beats installed pulseaudio" {
_detect_with "pgrep,pipewire,pulseaudio"
[ "$output" == "pipewire" ]
}

@test "detect_sound_server: pulse when only pulseaudio is installed" {
_detect_with "pgrep,pulseaudio"
[ "$output" == "pulse" ]
}

@test "detect_sound_server: alsa when only alsa-utils are installed" {
_detect_with "pgrep,aplay,amixer"
[ "$output" == "alsa" ]
}

@test "detect_sound_server: graceful error when nothing is available" {
_detect_with "pgrep"
[ "$status" -eq 1 ]
[[ "$output" == *"No sound server detected"* ]]
}
13 changes: 13 additions & 0 deletions test/fixtures/aplay_all_cards.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
**** List of PLAYBACK Hardware Devices ****
card 0: vc4hdmi0 [vc4-hdmi-0], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones]
Subdevices: 8/8
Subdevice #0: subdevice #0
card 2: wm8960soundcard [wm8960-soundcard], device 0: bcm2835-i2s-wm8960-hifi wm8960-hifi-0 [bcm2835-i2s-wm8960-hifi wm8960-hifi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 3: Device [USB Audio Device], device 0: USB Audio [USB Audio]
Subdevices: 1/1
Subdevice #0: subdevice #0
10 changes: 10 additions & 0 deletions test/fixtures/aplay_hat_only.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
**** List of PLAYBACK Hardware Devices ****
card 0: vc4hdmi0 [vc4-hdmi-0], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones]
Subdevices: 8/8
Subdevice #0: subdevice #0
card 2: sndrpihifiberry [snd_rpi_hifiberry_dacplus], device 0: HiFiBerry DAC+ Pro HiFi pcm512x-hifi-0 [HiFiBerry DAC+ Pro HiFi pcm512x-hifi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
4 changes: 4 additions & 0 deletions test/fixtures/aplay_hdmi_only.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**** List of PLAYBACK Hardware Devices ****
card 0: vc4hdmi0 [vc4-hdmi-0], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
1 change: 1 addition & 0 deletions test/fixtures/aplay_none.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**** List of PLAYBACK Hardware Devices ****
7 changes: 7 additions & 0 deletions test/fixtures/aplay_onboard_only.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
**** List of PLAYBACK Hardware Devices ****
card 0: vc4hdmi0 [vc4-hdmi-0], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones]
Subdevices: 8/8
Subdevice #0: subdevice #0
10 changes: 10 additions & 0 deletions test/fixtures/aplay_two_usb.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
**** List of PLAYBACK Hardware Devices ****
card 0: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones]
Subdevices: 8/8
Subdevice #0: subdevice #0
card 1: Device [USB Audio Device], device 0: USB Audio [USB Audio]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 2: Speaker [Plugable USB Audio Speaker], device 0: USB Audio [USB Audio]
Subdevices: 1/1
Subdevice #0: subdevice #0
21 changes: 21 additions & 0 deletions test/fixtures/pactl_list_sinks.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Sink #0
State: SUSPENDED
Name: alsa_output.platform-bcm2835_audio.stereo-fallback
Description: Built-in Audio Stereo
Driver: PipeWire
Sample Specification: s16le 2ch 48000Hz
Properties:
api.alsa.card = "1"
api.alsa.card.name = "bcm2835 Headphones"
node.name = "alsa_output.platform-bcm2835_audio.stereo-fallback"

Sink #1
State: RUNNING
Name: alsa_output.usb-0d8c_USB_Audio_Device-00.analog-stereo
Description: USB Audio Device Analog Stereo
Driver: PipeWire
Sample Specification: s16le 2ch 48000Hz
Properties:
api.alsa.card = "3"
api.alsa.card.name = "USB Audio Device"
node.name = "alsa_output.usb-0d8c_USB_Audio_Device-00.analog-stereo"
4 changes: 4 additions & 0 deletions test/fixtures/pactl_short_sinks.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
0 auto_null module-null-sink.c s16le 2ch 44100Hz SUSPENDED
1 alsa_output.platform-bcm2835_audio.stereo-fallback PipeWire s16le 2ch 48000Hz SUSPENDED
2 alsa_output.usb-0d8c_USB_Audio_Device-00.analog-stereo PipeWire s16le 2ch 48000Hz RUNNING
3 auto_combined module-combine-sink.c s16le 2ch 48000Hz IDLE
8 changes: 8 additions & 0 deletions test/fixtures/wpctl_inspect_50.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
id 50, type PipeWire:Interface:Node
api.alsa.card = "1"
api.alsa.pcm.card = "1"
api.alsa.path = "front:1"
audio.channels = "2"
node.name = "alsa_output.platform-bcm2835_audio.stereo-fallback"
node.description = "Built-in Audio Stereo"
media.class = "Audio/Sink"
8 changes: 8 additions & 0 deletions test/fixtures/wpctl_inspect_55.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
id 55, type PipeWire:Interface:Node
api.alsa.card = "3"
api.alsa.pcm.card = "3"
api.alsa.path = "front:3"
audio.channels = "2"
node.name = "alsa_output.usb-0d8c_USB_Audio_Device-00.analog-stereo"
node.description = "USB Audio Device Analog Stereo"
media.class = "Audio/Sink"
23 changes: 23 additions & 0 deletions test/fixtures/wpctl_status.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
PipeWire 'pipewire-0' [1.0.5, ovos@raspovos, cookie:12345]
└─ Clients:
32. WirePlumber [1.0.5, ovos@raspovos, pid:812]
33. WirePlumber [export] [1.0.5, ovos@raspovos, pid:812]

Audio
├─ Devices:
│ 41. Built-in Audio [alsa]
│ 42. USB Audio Device [alsa]
│
├─ Sinks:
│ * 50. Built-in Audio Stereo [vol: 0.74]
│ 55. USB Audio Device Analog Stereo [vol: 1.00]
│
├─ Sources:
│ 60. USB Audio Device Mono [vol: 1.00]
│
└─ Streams:

Video
├─ Devices:
│
└─ Sinks:
7 changes: 7 additions & 0 deletions test/stubs/aplay
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Test stub for aplay: prints the fixture pointed at by $APLAY_FIXTURE
if [ -z "${APLAY_FIXTURE:-}" ]; then
echo "aplay stub: APLAY_FIXTURE not set" >&2
exit 1
fi
cat "$APLAY_FIXTURE"
12 changes: 12 additions & 0 deletions test/stubs/pactl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Test stub for pactl:
# pactl list sinks -> cat $PACTL_SINKS_FIXTURE
# pactl list short sinks -> cat $PACTL_SHORT_SINKS_FIXTURE
if [ "$1" == "list" ] && [ "$2" == "short" ] && [ "$3" == "sinks" ]; then
cat "$PACTL_SHORT_SINKS_FIXTURE"
elif [ "$1" == "list" ] && [ "$2" == "sinks" ]; then
cat "$PACTL_SINKS_FIXTURE"
else
echo "pactl stub: unhandled args: $*" >&2
exit 1
fi
Loading
Loading