initial commit
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
package mixer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"rhemawavetech.com/rhemawave/internal/db"
|
||||
)
|
||||
|
||||
// Regression tests for critical audio bugs (v0.5)
|
||||
|
||||
// Regression: Stop() used to return early when AudioEnabled was false,
|
||||
// leaving music playing after the user toggled audio off mid-playback.
|
||||
func TestStopCleansUpWhenAudioDisabled(t *testing.T) {
|
||||
mu.Lock()
|
||||
wasInited := inited
|
||||
inited = true
|
||||
deviceOpen = false
|
||||
currentPath = "/some/track.wav"
|
||||
currentLoops = -1
|
||||
mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
mu.Lock()
|
||||
inited = wasInited
|
||||
currentPath = ""
|
||||
currentLoops = 0
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
cfg := &db.AppConfig{AudioEnabled: false}
|
||||
Stop(0, cfg)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if currentPath != "" {
|
||||
t.Errorf("currentPath = %q, want empty after Stop with AudioEnabled=false", currentPath)
|
||||
}
|
||||
if currentLoops != 0 {
|
||||
t.Errorf("currentLoops = %d, want 0 after Stop with AudioEnabled=false", currentLoops)
|
||||
}
|
||||
}
|
||||
|
||||
// Regression: SwitchDevice used to convert oldDevice="" to "system default"
|
||||
// and pass that string to openDeviceLocked, which SDL does not recognize.
|
||||
func TestSwitchDeviceReturnsErrorWhenUninitialized(t *testing.T) {
|
||||
mu.Lock()
|
||||
wasInited := inited
|
||||
inited = false
|
||||
deviceOpen = false
|
||||
mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
mu.Lock()
|
||||
inited = wasInited
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
cfg := &db.AppConfig{AudioEnabled: true, SavedVolume: 0.5}
|
||||
err := SwitchDevice("Some Device", cfg)
|
||||
if err == nil {
|
||||
t.Fatal("SwitchDevice on uninitialized mixer should return error")
|
||||
}
|
||||
}
|
||||
|
||||
// Regression: SwitchDevice with nil config must not panic when attempting
|
||||
// to restore volume after reopening the device.
|
||||
func TestSwitchDeviceNilConfigDoesNotPanic(t *testing.T) {
|
||||
mu.Lock()
|
||||
wasInited := inited
|
||||
inited = false
|
||||
deviceOpen = false
|
||||
mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
mu.Lock()
|
||||
inited = wasInited
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
err := SwitchDevice("Some Device", nil)
|
||||
if err == nil {
|
||||
t.Fatal("SwitchDevice on uninitialized mixer should return error")
|
||||
}
|
||||
}
|
||||
|
||||
// Regression: SwitchDevice with empty device name (system default) should
|
||||
// not be converted to "system default" before passing to openDeviceLocked.
|
||||
func TestSwitchDeviceAcceptsEmptyDeviceName(t *testing.T) {
|
||||
mu.Lock()
|
||||
wasInited := inited
|
||||
inited = false
|
||||
deviceOpen = false
|
||||
mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
mu.Lock()
|
||||
inited = wasInited
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
cfg := &db.AppConfig{SavedVolume: 0.75}
|
||||
err := SwitchDevice("", cfg)
|
||||
// Returns error because mixer is not initialized, but should not panic
|
||||
// and should handle the empty name gracefully.
|
||||
if err == nil {
|
||||
t.Fatal("SwitchDevice on uninitialized mixer should return error")
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user