Play() held mu then called SwitchDevice() which also tried to acquire mu, causing a deadlock. Replace with direct call to openDeviceLocked() which does not lock. Add regression test that verifies Play() returns an error (not deadlock) when called on an uninitialized mixer.
212 lines
4.6 KiB
Go
212 lines
4.6 KiB
Go
package mixer
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"rhemawavetech.com/rhemawave/internal/db"
|
|
)
|
|
|
|
// TestPlayDoesNotDeadlockWhenUninitialized verifies that Play() returns an error
|
|
// (not a deadlock) when the mixer has not been initialized.
|
|
// The old code would deadlock because Play() held mu then called SwitchDevice()
|
|
// which also tried to acquire mu.
|
|
func TestPlayDoesNotDeadlockWhenUninitialized(t *testing.T) {
|
|
// Ensure mixer is in uninitialized state
|
|
mu.Lock()
|
|
wasInited := inited
|
|
inited = false
|
|
deviceOpen = false
|
|
mu.Unlock()
|
|
|
|
defer func() {
|
|
mu.Lock()
|
|
inited = wasInited
|
|
mu.Unlock()
|
|
}()
|
|
|
|
cfg := &db.AppConfig{AudioEnabled: true}
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- Play("nonexistent.wav", false, cfg)
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err == nil {
|
|
t.Fatal("Play() returned nil error on uninitialized mixer, want error")
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Play() deadlocked on uninitialized mixer (did not return within 5s)")
|
|
}
|
|
}
|
|
|
|
func TestParseSinkLine(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantOK bool
|
|
wantID int
|
|
wantName string
|
|
wantVol float64
|
|
wantDef bool
|
|
}{
|
|
{
|
|
name: "default sink with volume",
|
|
input: "*51. USB Audio [vol: 1.00]",
|
|
wantOK: true,
|
|
wantID: 51,
|
|
wantName: "USB Audio",
|
|
wantVol: 1.00,
|
|
wantDef: true,
|
|
},
|
|
{
|
|
name: "non-default sink with volume",
|
|
input: "33. Built-in Audio Analog Stereo [vol: 0.65]",
|
|
wantOK: true,
|
|
wantID: 33,
|
|
wantName: "Built-in Audio Analog Stereo",
|
|
wantVol: 0.65,
|
|
wantDef: false,
|
|
},
|
|
{
|
|
name: "default sink with muted tag",
|
|
input: "*51. USB Audio [vol: 0.50 MUTED]",
|
|
wantOK: true,
|
|
wantID: 51,
|
|
wantName: "USB Audio",
|
|
wantVol: 0.50,
|
|
wantDef: true,
|
|
},
|
|
{
|
|
name: "sink without volume field",
|
|
input: "33. Built-in Audio Analog Stereo",
|
|
wantOK: true,
|
|
wantID: 33,
|
|
wantName: "Built-in Audio Analog Stereo",
|
|
wantVol: 0.0,
|
|
wantDef: false,
|
|
},
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "no dot separator",
|
|
input: "no dot here",
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "non-numeric ID",
|
|
input: "abc. Some Device [vol: 0.80]",
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "zero volume",
|
|
input: "1. Muted Device [vol: 0.00]",
|
|
wantOK: true,
|
|
wantID: 1,
|
|
wantName: "Muted Device",
|
|
wantVol: 0.0,
|
|
wantDef: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sink, ok := parseSinkLine(tt.input)
|
|
if ok != tt.wantOK {
|
|
t.Fatalf("parseSinkLine(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
|
|
}
|
|
if !ok {
|
|
return
|
|
}
|
|
if sink.ID != tt.wantID {
|
|
t.Errorf("ID = %d, want %d", sink.ID, tt.wantID)
|
|
}
|
|
if sink.Name != tt.wantName {
|
|
t.Errorf("Name = %q, want %q", sink.Name, tt.wantName)
|
|
}
|
|
if sink.Volume != tt.wantVol {
|
|
t.Errorf("Volume = %f, want %f", sink.Volume, tt.wantVol)
|
|
}
|
|
if sink.IsDefault != tt.wantDef {
|
|
t.Errorf("IsDefault = %v, want %v", sink.IsDefault, tt.wantDef)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSinks(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantCount int
|
|
wantNames []string
|
|
}{
|
|
{
|
|
name: "two sinks",
|
|
input: `Audio
|
|
Sinks:
|
|
*51. USB Audio [vol: 1.00]
|
|
33. Built-in Audio Analog Stereo [vol: 0.65]
|
|
Sources:
|
|
52. Microphone [vol: 0.80]`,
|
|
wantCount: 2,
|
|
wantNames: []string{"USB Audio", "Built-in Audio Analog Stereo"},
|
|
},
|
|
{
|
|
name: "no sinks section",
|
|
input: `Audio
|
|
Sources:
|
|
52. Microphone [vol: 0.80]`,
|
|
wantCount: 0,
|
|
},
|
|
{
|
|
name: "sinks with tree characters",
|
|
input: `Audio
|
|
├─Sinks:
|
|
│ ├─*51. USB Audio [vol: 1.00]
|
|
│ └─33. Built-in Audio Analog Stereo [vol: 0.65]
|
|
└─Sources:
|
|
└─52. Microphone [vol: 0.80]`,
|
|
wantCount: 2,
|
|
wantNames: []string{"USB Audio", "Built-in Audio Analog Stereo"},
|
|
},
|
|
{
|
|
name: "empty input",
|
|
input: ``,
|
|
wantCount: 0,
|
|
},
|
|
{
|
|
name: "video section ignored",
|
|
input: `Video
|
|
Sinks:
|
|
100. HDMI Output [vol: 1.00]
|
|
Audio
|
|
Sinks:
|
|
51. USB Audio [vol: 1.00]`,
|
|
wantCount: 1,
|
|
wantNames: []string{"USB Audio"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sinks, err := parseSinks([]byte(tt.input))
|
|
if err != nil {
|
|
t.Fatalf("parseSinks returned error: %v", err)
|
|
}
|
|
if len(sinks) != tt.wantCount {
|
|
t.Fatalf("got %d sinks, want %d", len(sinks), tt.wantCount)
|
|
}
|
|
for i, name := range tt.wantNames {
|
|
if sinks[i].Name != name {
|
|
t.Errorf("sink[%d].Name = %q, want %q", i, sinks[i].Name, name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|