401 lines
10 KiB
Go
401 lines
10 KiB
Go
package ipc
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/bjarneo/cliamp/internal/playback"
|
|
)
|
|
|
|
// captureDispatcher records the last message sent via Send and optionally
|
|
// auto-replies on reply channels so handler cases with a Reply can be tested
|
|
// without spinning up a real UI goroutine.
|
|
type captureDispatcher struct {
|
|
last any
|
|
autoReply Response
|
|
}
|
|
|
|
func (c *captureDispatcher) Send(msg any) {
|
|
c.last = msg
|
|
switch m := msg.(type) {
|
|
case LoadMsg:
|
|
m.Reply <- c.autoReply
|
|
case ThemeMsg:
|
|
m.Reply <- c.autoReply
|
|
case VisMsg:
|
|
m.Reply <- c.autoReply
|
|
case ShuffleMsg:
|
|
m.Reply <- c.autoReply
|
|
case RepeatMsg:
|
|
m.Reply <- c.autoReply
|
|
case MonoMsg:
|
|
m.Reply <- c.autoReply
|
|
case SpeedMsg:
|
|
m.Reply <- c.autoReply
|
|
case EQMsg:
|
|
m.Reply <- c.autoReply
|
|
case DeviceMsg:
|
|
m.Reply <- c.autoReply
|
|
case StatusRequestMsg:
|
|
m.Reply <- c.autoReply
|
|
}
|
|
}
|
|
|
|
func newTestServer(disp Dispatcher) *Server {
|
|
return &Server{disp: disp, done: make(chan struct{})}
|
|
}
|
|
|
|
func TestDispatchSimpleCommands(t *testing.T) {
|
|
tests := []struct {
|
|
cmd string
|
|
check func(t *testing.T, got any)
|
|
okWant bool
|
|
}{
|
|
{"play", func(t *testing.T, got any) {
|
|
if _, ok := got.(PlayMsg); !ok {
|
|
t.Errorf("got %T, want PlayMsg", got)
|
|
}
|
|
}, true},
|
|
{"pause", func(t *testing.T, got any) {
|
|
if _, ok := got.(PauseMsg); !ok {
|
|
t.Errorf("got %T, want PauseMsg", got)
|
|
}
|
|
}, true},
|
|
{"toggle", func(t *testing.T, got any) {
|
|
if _, ok := got.(playback.PlayPauseMsg); !ok {
|
|
t.Errorf("got %T, want playback.PlayPauseMsg", got)
|
|
}
|
|
}, true},
|
|
{"stop", func(t *testing.T, got any) {
|
|
if _, ok := got.(playback.StopMsg); !ok {
|
|
t.Errorf("got %T, want playback.StopMsg", got)
|
|
}
|
|
}, true},
|
|
{"next", func(t *testing.T, got any) {
|
|
if _, ok := got.(playback.NextMsg); !ok {
|
|
t.Errorf("got %T, want playback.NextMsg", got)
|
|
}
|
|
}, true},
|
|
{"prev", func(t *testing.T, got any) {
|
|
if _, ok := got.(playback.PrevMsg); !ok {
|
|
t.Errorf("got %T, want playback.PrevMsg", got)
|
|
}
|
|
}, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.cmd, func(t *testing.T) {
|
|
disp := &captureDispatcher{}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: tt.cmd})
|
|
if resp.OK != tt.okWant {
|
|
t.Errorf("OK = %v, want %v (err=%q)", resp.OK, tt.okWant, resp.Error)
|
|
}
|
|
tt.check(t, disp.last)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDispatchUppercaseCommand(t *testing.T) {
|
|
disp := &captureDispatcher{}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "PLAY"})
|
|
if !resp.OK {
|
|
t.Fatalf("uppercase cmd PLAY should still be accepted, got err=%q", resp.Error)
|
|
}
|
|
if _, ok := disp.last.(PlayMsg); !ok {
|
|
t.Errorf("got %T, want PlayMsg", disp.last)
|
|
}
|
|
}
|
|
|
|
func TestDispatchVolume(t *testing.T) {
|
|
disp := &captureDispatcher{}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "volume", Value: -3.5})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
got, ok := disp.last.(VolumeMsg)
|
|
if !ok {
|
|
t.Fatalf("got %T, want VolumeMsg", disp.last)
|
|
}
|
|
if got.DB != -3.5 {
|
|
t.Errorf("DB = %f, want -3.5", got.DB)
|
|
}
|
|
}
|
|
|
|
func TestDispatchSeek(t *testing.T) {
|
|
disp := &captureDispatcher{}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "seek", Value: 1.5})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
got, ok := disp.last.(SeekMsg)
|
|
if !ok {
|
|
t.Fatalf("got %T, want SeekMsg", disp.last)
|
|
}
|
|
if got.Offset != 1500*time.Millisecond {
|
|
t.Errorf("Offset = %v, want 1.5s", got.Offset)
|
|
}
|
|
}
|
|
|
|
func TestDispatchLoadMissingPlaylist(t *testing.T) {
|
|
s := newTestServer(&captureDispatcher{})
|
|
resp := s.dispatch(Request{Cmd: "load"})
|
|
if resp.OK {
|
|
t.Error("load without playlist should return !OK")
|
|
}
|
|
if resp.Error == "" {
|
|
t.Error("error message should be set")
|
|
}
|
|
}
|
|
|
|
func TestDispatchLoadWithReply(t *testing.T) {
|
|
disp := &captureDispatcher{autoReply: Response{OK: true, Playlist: "main"}}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "load", Playlist: "main"})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
if resp.Playlist != "main" {
|
|
t.Errorf("Playlist = %q, want main", resp.Playlist)
|
|
}
|
|
}
|
|
|
|
func TestDispatchQueueMissingPath(t *testing.T) {
|
|
s := newTestServer(&captureDispatcher{})
|
|
resp := s.dispatch(Request{Cmd: "queue"})
|
|
if resp.OK {
|
|
t.Error("queue without path should return !OK")
|
|
}
|
|
}
|
|
|
|
func TestDispatchQueue(t *testing.T) {
|
|
disp := &captureDispatcher{}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "queue", Path: "/music/song.mp3"})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
got, ok := disp.last.(QueueMsg)
|
|
if !ok {
|
|
t.Fatalf("got %T, want QueueMsg", disp.last)
|
|
}
|
|
if got.Path != "/music/song.mp3" {
|
|
t.Errorf("Path = %q, want /music/song.mp3", got.Path)
|
|
}
|
|
}
|
|
|
|
func TestDispatchThemeMissingName(t *testing.T) {
|
|
s := newTestServer(&captureDispatcher{})
|
|
resp := s.dispatch(Request{Cmd: "theme"})
|
|
if resp.OK {
|
|
t.Error("theme without name should return !OK")
|
|
}
|
|
}
|
|
|
|
func TestDispatchThemeWithReply(t *testing.T) {
|
|
disp := &captureDispatcher{autoReply: Response{OK: true}}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "theme", Name: "dracula"})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
got, ok := disp.last.(ThemeMsg)
|
|
if !ok {
|
|
t.Fatalf("got %T, want ThemeMsg", disp.last)
|
|
}
|
|
if got.Name != "dracula" {
|
|
t.Errorf("Name = %q, want dracula", got.Name)
|
|
}
|
|
}
|
|
|
|
func TestDispatchVisMissingName(t *testing.T) {
|
|
s := newTestServer(&captureDispatcher{})
|
|
resp := s.dispatch(Request{Cmd: "vis"})
|
|
if resp.OK {
|
|
t.Error("vis without name should return !OK")
|
|
}
|
|
}
|
|
|
|
func TestDispatchVisWithReply(t *testing.T) {
|
|
disp := &captureDispatcher{autoReply: Response{OK: true, Visualizer: "bars"}}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "vis", Name: "bars"})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
if resp.Visualizer != "bars" {
|
|
t.Errorf("Visualizer = %q, want bars", resp.Visualizer)
|
|
}
|
|
}
|
|
|
|
func TestDispatchShuffle(t *testing.T) {
|
|
trueBool := true
|
|
disp := &captureDispatcher{autoReply: Response{OK: true, Shuffle: &trueBool}}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "shuffle", Name: "on"})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
got, ok := disp.last.(ShuffleMsg)
|
|
if !ok {
|
|
t.Fatalf("got %T, want ShuffleMsg", disp.last)
|
|
}
|
|
if got.Name != "on" {
|
|
t.Errorf("Name = %q, want on", got.Name)
|
|
}
|
|
}
|
|
|
|
func TestDispatchRepeat(t *testing.T) {
|
|
disp := &captureDispatcher{autoReply: Response{OK: true, Repeat: "all"}}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "repeat", Name: "all"})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
if _, ok := disp.last.(RepeatMsg); !ok {
|
|
t.Errorf("got %T, want RepeatMsg", disp.last)
|
|
}
|
|
}
|
|
|
|
func TestDispatchMono(t *testing.T) {
|
|
trueBool := true
|
|
disp := &captureDispatcher{autoReply: Response{OK: true, Mono: &trueBool}}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "mono", Name: "toggle"})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
if _, ok := disp.last.(MonoMsg); !ok {
|
|
t.Errorf("got %T, want MonoMsg", disp.last)
|
|
}
|
|
}
|
|
|
|
func TestDispatchSpeedInvalid(t *testing.T) {
|
|
tests := []float64{0, -1, -0.5}
|
|
for _, v := range tests {
|
|
s := newTestServer(&captureDispatcher{})
|
|
resp := s.dispatch(Request{Cmd: "speed", Value: v})
|
|
if resp.OK {
|
|
t.Errorf("speed with value=%v should return !OK", v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDispatchSpeedValid(t *testing.T) {
|
|
disp := &captureDispatcher{autoReply: Response{OK: true, Speed: 1.25}}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "speed", Value: 1.25})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
got, ok := disp.last.(SpeedMsg)
|
|
if !ok {
|
|
t.Fatalf("got %T, want SpeedMsg", disp.last)
|
|
}
|
|
if got.Speed != 1.25 {
|
|
t.Errorf("Speed = %f, want 1.25", got.Speed)
|
|
}
|
|
}
|
|
|
|
func TestDispatchEQ(t *testing.T) {
|
|
disp := &captureDispatcher{autoReply: Response{OK: true}}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "eq", Name: "rock", Band: 3, Value: 4.5})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
got, ok := disp.last.(EQMsg)
|
|
if !ok {
|
|
t.Fatalf("got %T, want EQMsg", disp.last)
|
|
}
|
|
if got.Name != "rock" || got.Band != 3 || got.Value != 4.5 {
|
|
t.Errorf("EQMsg = %+v, want Name=rock Band=3 Value=4.5", got)
|
|
}
|
|
}
|
|
|
|
func TestDispatchDeviceMissingName(t *testing.T) {
|
|
s := newTestServer(&captureDispatcher{})
|
|
resp := s.dispatch(Request{Cmd: "device"})
|
|
if resp.OK {
|
|
t.Error("device without name should return !OK")
|
|
}
|
|
}
|
|
|
|
func TestDispatchDevice(t *testing.T) {
|
|
disp := &captureDispatcher{autoReply: Response{OK: true, Device: "alsa:default"}}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "device", Name: "list"})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
if resp.Device != "alsa:default" {
|
|
t.Errorf("Device = %q, want alsa:default", resp.Device)
|
|
}
|
|
}
|
|
|
|
func TestDispatchStatus(t *testing.T) {
|
|
disp := &captureDispatcher{autoReply: Response{OK: true, State: "playing", Position: 30}}
|
|
s := newTestServer(disp)
|
|
resp := s.dispatch(Request{Cmd: "status"})
|
|
if !resp.OK {
|
|
t.Fatalf("OK = false, err=%q", resp.Error)
|
|
}
|
|
if resp.State != "playing" {
|
|
t.Errorf("State = %q, want playing", resp.State)
|
|
}
|
|
if _, ok := disp.last.(StatusRequestMsg); !ok {
|
|
t.Errorf("got %T, want StatusRequestMsg", disp.last)
|
|
}
|
|
}
|
|
|
|
func TestDispatchUnknownCmd(t *testing.T) {
|
|
s := newTestServer(&captureDispatcher{})
|
|
resp := s.dispatch(Request{Cmd: "dostuff"})
|
|
if resp.OK {
|
|
t.Error("unknown cmd should return !OK")
|
|
}
|
|
if resp.Error == "" {
|
|
t.Error("error should be set for unknown cmd")
|
|
}
|
|
}
|
|
|
|
// deadDispatcher never replies on the Reply channel — triggers the timeout branch.
|
|
type deadDispatcher struct{}
|
|
|
|
func (deadDispatcher) Send(msg any) {}
|
|
|
|
func TestDispatchReplyTimeout(t *testing.T) {
|
|
// Replace time.After with a fast timeout to keep this test cheap.
|
|
// The production code uses 3s; a test using real time would be slow,
|
|
// so we exercise the shutdown-path via s.done instead.
|
|
s := newTestServer(deadDispatcher{})
|
|
close(s.done) // simulate server shutting down
|
|
|
|
tests := []Request{
|
|
{Cmd: "load", Playlist: "main"},
|
|
{Cmd: "theme", Name: "dracula"},
|
|
{Cmd: "vis", Name: "bars"},
|
|
{Cmd: "shuffle", Name: "on"},
|
|
{Cmd: "repeat", Name: "all"},
|
|
{Cmd: "mono", Name: "toggle"},
|
|
{Cmd: "speed", Value: 1.0},
|
|
{Cmd: "eq", Name: "rock"},
|
|
{Cmd: "device", Name: "list"},
|
|
{Cmd: "status"},
|
|
}
|
|
for _, r := range tests {
|
|
t.Run(r.Cmd, func(t *testing.T) {
|
|
resp := s.dispatch(r)
|
|
if resp.OK {
|
|
t.Errorf("%s should return !OK during shutdown", r.Cmd)
|
|
}
|
|
if resp.Error == "" {
|
|
t.Errorf("%s should set error message during shutdown", r.Cmd)
|
|
}
|
|
})
|
|
}
|
|
}
|