233 lines
6.8 KiB
Go
233 lines
6.8 KiB
Go
// ABOUTME: Tests for the pure matchDevice selection logic used by Open
|
|
package output
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gen2brain/malgo"
|
|
)
|
|
|
|
// newDevice builds a PlaybackDevice with a unique sentinel ID so tests can
|
|
// assert the correct entry was returned. The actual ID bytes are opaque to
|
|
// miniaudio at this layer; we only check that matchDevice returns the right
|
|
// slice element.
|
|
func newDevice(name string, isDefault bool, marker byte) PlaybackDevice {
|
|
var id malgo.DeviceID
|
|
id[0] = marker
|
|
return PlaybackDevice{Name: name, IsDefault: isDefault, ID: id}
|
|
}
|
|
|
|
func TestMatchDevice_EmptyRequest(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
devices []PlaybackDevice
|
|
wantNil bool
|
|
wantName string
|
|
wantMarker byte
|
|
}{
|
|
{
|
|
name: "empty catalog returns nil",
|
|
devices: nil,
|
|
wantNil: true,
|
|
},
|
|
{
|
|
name: "prefers the device flagged IsDefault",
|
|
devices: []PlaybackDevice{
|
|
newDevice("First", false, 0x01),
|
|
newDevice("DefaultSink", true, 0x02),
|
|
newDevice("Third", false, 0x03),
|
|
},
|
|
wantName: "DefaultSink",
|
|
wantMarker: 0x02,
|
|
},
|
|
{
|
|
name: "falls back to first device when none flagged default",
|
|
devices: []PlaybackDevice{
|
|
newDevice("Alpha", false, 0x0A),
|
|
newDevice("Beta", false, 0x0B),
|
|
},
|
|
wantName: "Alpha",
|
|
wantMarker: 0x0A,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := matchDevice(tt.devices, "")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if tt.wantNil {
|
|
if got != nil {
|
|
t.Errorf("expected nil, got %+v", got)
|
|
}
|
|
return
|
|
}
|
|
if got == nil {
|
|
t.Fatal("expected a device, got nil")
|
|
}
|
|
if got.Name != tt.wantName {
|
|
t.Errorf("name = %q, want %q", got.Name, tt.wantName)
|
|
}
|
|
if got.ID[0] != tt.wantMarker {
|
|
t.Errorf("id[0] = 0x%x, want 0x%x (wrong slice element returned)", got.ID[0], tt.wantMarker)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMatchDevice_ExactNameMatch(t *testing.T) {
|
|
devices := []PlaybackDevice{
|
|
newDevice("HDA Intel PCH: ALC257 Analog", true, 0x10),
|
|
newDevice("HDMI 0", false, 0x11),
|
|
newDevice("USB Audio Device", false, 0x12),
|
|
}
|
|
|
|
got, err := matchDevice(devices, "USB Audio Device")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got == nil || got.Name != "USB Audio Device" {
|
|
t.Errorf("got %+v, want USB Audio Device", got)
|
|
}
|
|
if got.ID[0] != 0x12 {
|
|
t.Errorf("wrong device matched: id[0] = 0x%x, want 0x12", got.ID[0])
|
|
}
|
|
}
|
|
|
|
func TestMatchDevice_NoMatchListsAvailable(t *testing.T) {
|
|
devices := []PlaybackDevice{
|
|
newDevice("Charlie", false, 0x01),
|
|
newDevice("Alpha", true, 0x02),
|
|
newDevice("Bravo", false, 0x03),
|
|
}
|
|
|
|
got, err := matchDevice(devices, "DoesNotExist")
|
|
if got != nil {
|
|
t.Errorf("expected nil device, got %+v", got)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, `"DoesNotExist"`) {
|
|
t.Errorf("error should name the missing device: %q", msg)
|
|
}
|
|
// Available names must be listed, sorted, so users can copy/paste the right one.
|
|
for _, want := range []string{"Alpha", "Bravo", "Charlie"} {
|
|
if !strings.Contains(msg, want) {
|
|
t.Errorf("error should list %q; got %q", want, msg)
|
|
}
|
|
}
|
|
alphaIdx := strings.Index(msg, "Alpha")
|
|
bravoIdx := strings.Index(msg, "Bravo")
|
|
charlieIdx := strings.Index(msg, "Charlie")
|
|
if !(alphaIdx < bravoIdx && bravoIdx < charlieIdx) {
|
|
t.Errorf("available names should be sorted alphabetically; got %q", msg)
|
|
}
|
|
}
|
|
|
|
func TestMatchDevice_NoMatchEmptyCatalogGivesDistinctError(t *testing.T) {
|
|
got, err := matchDevice(nil, "Anything")
|
|
if got != nil {
|
|
t.Errorf("expected nil device, got %+v", got)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, "no playback devices available") {
|
|
t.Errorf("error should distinguish empty-catalog case: %q", msg)
|
|
}
|
|
}
|
|
|
|
// TestMatchDevice_ShortNameMatch covers miniaudio's Linux/ALSA naming where
|
|
// device.name is "<card-short>, <stream-description>" — users typing just
|
|
// the short prefix should match unambiguously when only one device has that
|
|
// prefix. Reproduces the HiFiBerry case from the field bug.
|
|
func TestMatchDevice_ShortNameMatch(t *testing.T) {
|
|
devices := []PlaybackDevice{
|
|
newDevice("Default Audio Device", true, 0x01),
|
|
newDevice("vc4-hdmi-0, MAI PCM i2s-hifi-0", false, 0x02),
|
|
newDevice("vc4-hdmi-1, MAI PCM i2s-hifi-0", false, 0x03),
|
|
newDevice("PDP Audio Device, USB Audio", false, 0x04),
|
|
}
|
|
|
|
got, err := matchDevice(devices, "vc4-hdmi-0")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got == nil || got.ID[0] != 0x02 {
|
|
t.Errorf("short-name %q should resolve to id 0x02; got %+v", "vc4-hdmi-0", got)
|
|
}
|
|
|
|
got, err = matchDevice(devices, "PDP Audio Device")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got == nil || got.ID[0] != 0x04 {
|
|
t.Errorf("short-name %q should resolve to id 0x04; got %+v", "PDP Audio Device", got)
|
|
}
|
|
}
|
|
|
|
// TestMatchDevice_ExactNameWinsOverShortName guards the precedence: if a
|
|
// device's full name happens to equal someone else's short prefix, the
|
|
// exact match takes priority over the short-name search.
|
|
func TestMatchDevice_ExactNameWinsOverShortName(t *testing.T) {
|
|
devices := []PlaybackDevice{
|
|
newDevice("Foo, long description", false, 0x01),
|
|
newDevice("Foo", false, 0x02),
|
|
}
|
|
|
|
got, err := matchDevice(devices, "Foo")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got == nil || got.ID[0] != 0x02 {
|
|
t.Errorf("exact match should win: expected id 0x02; got %+v", got)
|
|
}
|
|
}
|
|
|
|
// TestMatchDevice_ShortNameAmbiguousReturnsError covers the two-HiFiBerry
|
|
// case: the same short prefix matches multiple devices. We must not silently
|
|
// pick one.
|
|
func TestMatchDevice_ShortNameAmbiguousReturnsError(t *testing.T) {
|
|
devices := []PlaybackDevice{
|
|
newDevice("HiFiBerry, card 0", false, 0x01),
|
|
newDevice("HiFiBerry, card 1", false, 0x02),
|
|
}
|
|
|
|
got, err := matchDevice(devices, "HiFiBerry")
|
|
if got != nil {
|
|
t.Errorf("expected nil on ambiguous short-name match, got %+v", got)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("expected an error on ambiguous short-name match")
|
|
}
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, "ambiguous") {
|
|
t.Errorf("error should mention ambiguity: %q", msg)
|
|
}
|
|
if !strings.Contains(msg, `"HiFiBerry, card 0"`) || !strings.Contains(msg, `"HiFiBerry, card 1"`) {
|
|
t.Errorf("ambiguity error should list both candidates quoted with %%q: %q", msg)
|
|
}
|
|
}
|
|
|
|
// TestMatchDevice_NoMatchQuotesNames ensures names with embedded commas are
|
|
// distinguishable from the list separator in the error output.
|
|
func TestMatchDevice_NoMatchQuotesNames(t *testing.T) {
|
|
devices := []PlaybackDevice{
|
|
newDevice("vc4-hdmi-0, MAI PCM i2s-hifi-0", false, 0x01),
|
|
}
|
|
|
|
_, err := matchDevice(devices, "nonexistent")
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, `"vc4-hdmi-0, MAI PCM i2s-hifi-0"`) {
|
|
t.Errorf("name should appear quoted in error so embedded comma is unambiguous: %q", msg)
|
|
}
|
|
}
|