Files
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

266 lines
5.7 KiB
Go

package player
import (
"math"
"sync"
"testing"
)
// newTestPlayer returns a Player with only the atomic accessors wired up.
// We deliberately skip speaker.Init (which would open a real audio device)
// by constructing the struct directly — this limits us to testing the
// lock-free getters/setters, not the audio pipeline itself.
func newTestPlayer() *Player {
p := &Player{}
p.volMin.Store(math.Float64bits(-50))
p.speed.Store(math.Float64bits(1.0))
return p
}
func TestSetVolumeClamps(t *testing.T) {
p := newTestPlayer()
tests := []struct {
in float64
want float64
}{
{-60, -50}, // below min
{-50, -50},
{0, 0},
{6, 6},
{12, 6}, // above max
}
for _, tt := range tests {
p.SetVolume(tt.in)
if got := p.Volume(); got != tt.want {
t.Errorf("SetVolume(%v) → Volume() = %v, want %v", tt.in, got, tt.want)
}
}
}
func TestSetVolumeMinClamps(t *testing.T) {
p := newTestPlayer()
tests := []struct {
in float64
want float64
}{
{-100, -90}, // below absolute floor
{-90, -90},
{-50, -50},
{0, 0},
{5, 0}, // above max (must be ≤ 0)
}
for _, tt := range tests {
p.SetVolumeMin(tt.in)
if got := p.VolumeMin(); got != tt.want {
t.Errorf("SetVolumeMin(%v) → VolumeMin() = %v, want %v", tt.in, got, tt.want)
}
}
}
func TestSetVolumeMinReconcilesCurrent(t *testing.T) {
p := newTestPlayer()
p.SetVolume(-40)
p.SetVolumeMin(-30) // raise floor above current volume
if got := p.Volume(); got != -30 {
t.Errorf("Volume after SetVolumeMin raise = %v, want -30", got)
}
}
func TestVolumeClampedToCustomMin(t *testing.T) {
tests := []struct {
name string
volumeMin float64
volume float64
want float64
}{
{"below floor", -30, -40, -30},
{"at floor", -30, -30, -30},
{"above floor", -30, -10, -10},
{"custom floor -60", -60, -70, -60},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := newTestPlayer()
p.SetVolumeMin(tt.volumeMin)
p.SetVolume(tt.volume)
if got := p.Volume(); got != tt.want {
t.Errorf("Volume = %v, want %v", got, tt.want)
}
})
}
}
func TestSetSpeedClamps(t *testing.T) {
p := newTestPlayer()
tests := []struct {
in float64
want float64
}{
{0.1, 0.25},
{0.25, 0.25},
{1.0, 1.0},
{2.0, 2.0},
{3.0, 2.0},
}
for _, tt := range tests {
p.SetSpeed(tt.in)
if got := p.Speed(); got != tt.want {
t.Errorf("SetSpeed(%v) → Speed() = %v, want %v", tt.in, got, tt.want)
}
}
}
func TestToggleMono(t *testing.T) {
p := newTestPlayer()
if p.Mono() {
t.Fatal("Mono() should start false")
}
p.ToggleMono()
if !p.Mono() {
t.Error("ToggleMono should flip to true")
}
p.ToggleMono()
if p.Mono() {
t.Error("ToggleMono should flip back to false")
}
}
func TestSetEQBandClamps(t *testing.T) {
p := newTestPlayer()
tests := []struct {
band int
in float64
want float64
}{
{0, 20.0, 12.0},
{0, -20.0, -12.0},
{5, 6.5, 6.5},
{9, 0.0, 0.0},
}
for _, tt := range tests {
p.SetEQBand(tt.band, tt.in)
bands := p.EQBands()
if bands[tt.band] != tt.want {
t.Errorf("SetEQBand(%d, %v) → %v, want %v", tt.band, tt.in, bands[tt.band], tt.want)
}
}
}
func TestSetEQBandIgnoresInvalidIndex(t *testing.T) {
p := newTestPlayer()
// Setting an out-of-range band should be a no-op, not panic.
p.SetEQBand(-1, 5)
p.SetEQBand(10, 5)
p.SetEQBand(100, 5)
for i, b := range p.EQBands() {
if b != 0 {
t.Errorf("EQBands[%d] = %v, want 0 after invalid writes", i, b)
}
}
}
func TestEQBandsReturnsCopy(t *testing.T) {
p := newTestPlayer()
p.SetEQBand(0, 6)
bands := p.EQBands()
bands[0] = 999 // mutate local copy
if p.EQBands()[0] != 6 {
t.Error("EQBands() should return a copy — mutation leaked back")
}
}
func TestIsPlayingDefaultsFalse(t *testing.T) {
p := newTestPlayer()
if p.IsPlaying() {
t.Error("IsPlaying() should start false")
}
if p.IsPaused() {
t.Error("IsPaused() should start false")
}
}
func TestSampleRate(t *testing.T) {
p := &Player{sr: 44100}
if p.SampleRate() != 44100 {
t.Errorf("SampleRate() = %d, want 44100", p.SampleRate())
}
}
func TestStreamTitleEmpty(t *testing.T) {
p := newTestPlayer()
if got := p.StreamTitle(); got != "" {
t.Errorf("StreamTitle() on fresh player = %q, want empty", got)
}
}
func TestSetStreamTitle(t *testing.T) {
p := newTestPlayer()
p.setStreamTitle("Artist - Song")
if got := p.StreamTitle(); got != "Artist - Song" {
t.Errorf("StreamTitle() = %q, want 'Artist - Song'", got)
}
}
func TestRegisterStreamerFactory(t *testing.T) {
p := newTestPlayer()
var noop StreamerFactory // nil factory is fine for this storage-only test
p.RegisterStreamerFactory("spotify:", noop)
p.mu.Lock()
defer p.mu.Unlock()
if len(p.customFactories) != 1 {
t.Errorf("len(customFactories) = %d, want 1", len(p.customFactories))
}
if _, ok := p.customFactories["spotify:"]; !ok {
t.Error("factory not stored under 'spotify:'")
}
}
func TestRegisterBufferedURLMatcher(t *testing.T) {
p := newTestPlayer()
match := func(u string) bool { return u == "foo" }
p.RegisterBufferedURLMatcher(match)
p.mu.Lock()
defer p.mu.Unlock()
if p.bufferedURLMatch == nil {
t.Error("matcher not stored")
}
}
func TestConcurrentVolumeSetRead(t *testing.T) {
p := newTestPlayer()
var wg sync.WaitGroup
const N = 100
wg.Add(N * 2)
for i := 0; i < N; i++ {
go func(i int) {
defer wg.Done()
p.SetVolume(float64(-i % 30))
}(i)
go func() {
defer wg.Done()
_ = p.Volume()
}()
}
wg.Wait()
// If the race detector didn't flag anything, we're happy.
}
// Ensure the atomic uint64 storage actually wraps dB as expected.
func TestVolumeStoragePrecision(t *testing.T) {
p := newTestPlayer()
p.SetVolume(-3.14)
v := math.Float64frombits(p.volume.Load())
if math.Abs(v-(-3.14)) > 1e-12 {
t.Errorf("stored volume = %v, want -3.14", v)
}
}