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

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)
}
})
}
}