Refactoring #3
@@ -23,6 +23,7 @@ type PlaylistState struct {
|
|||||||
var (
|
var (
|
||||||
ErrPlaylistEmpty = errors.New("playlist is empty")
|
ErrPlaylistEmpty = errors.New("playlist is empty")
|
||||||
ErrPlaylistIndexOutOfRange = errors.New("playlist index is out of range")
|
ErrPlaylistIndexOutOfRange = errors.New("playlist index is out of range")
|
||||||
|
ErrPlaylistNoSelection = errors.New("playlist has no selected entry")
|
||||||
ErrUnknownPlaylistCommand = errors.New("unknown playlist command")
|
ErrUnknownPlaylistCommand = errors.New("unknown playlist command")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,3 +92,39 @@ func (s PlaylistState) Entry(playlist Playlist) (PlaylistEntry, bool) {
|
|||||||
}
|
}
|
||||||
return playlist.Entries[s.CurrentIndex], true
|
return playlist.Entries[s.CurrentIndex], true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ApplyPlaylistSelection(
|
||||||
|
playlist Playlist,
|
||||||
|
current PlaylistState,
|
||||||
|
command PlaylistCommand,
|
||||||
|
retry RetryPolicy,
|
||||||
|
) (
|
||||||
|
next PlaylistState,
|
||||||
|
sessionCommand SessionCommand,
|
||||||
|
apply bool,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
next, err = ApplyPlaylistCommand(playlist, current, command)
|
||||||
|
if err != nil {
|
||||||
|
return current, SessionCommand{}, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apply = command.Kind == PlaylistSelect || next != current
|
||||||
|
if !apply {
|
||||||
|
return next, SessionCommand{}, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, ok := next.Entry(playlist)
|
||||||
|
if !ok {
|
||||||
|
return current, SessionCommand{}, false, ErrPlaylistNoSelection
|
||||||
|
}
|
||||||
|
session := entry.SessionConfig(retry)
|
||||||
|
if err := session.Validate(); err != nil {
|
||||||
|
return current, SessionCommand{}, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return next, SessionCommand{
|
||||||
|
Kind: CommandSetSession,
|
||||||
|
Session: session,
|
||||||
|
}, true, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package playback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApplyPlaylistSelectionFirstNext(t *testing.T) {
|
||||||
|
retry := validPlaylistRetryPolicy()
|
||||||
|
|
||||||
|
next, command, apply, err := ApplyPlaylistSelection(
|
||||||
|
navigationPlaylist(false),
|
||||||
|
PlaylistState{},
|
||||||
|
PlaylistCommand{Kind: PlaylistNext},
|
||||||
|
retry,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() error = %v", err)
|
||||||
|
}
|
||||||
|
if !apply {
|
||||||
|
t.Fatal("ApplyPlaylistSelection() apply = false, want true")
|
||||||
|
}
|
||||||
|
wantState := PlaylistState{CurrentIndex: 0, HasSelection: true}
|
||||||
|
if next != wantState {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() state = %#v, want %#v", next, wantState)
|
||||||
|
}
|
||||||
|
wantCommand := SessionCommand{
|
||||||
|
Kind: CommandSetSession,
|
||||||
|
Session: SessionConfig{
|
||||||
|
Video: FeedConfig{Domain: "domain", UUID: "video-1", Active: true},
|
||||||
|
Retry: retry,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if command != wantCommand {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() command = %#v, want %#v", command, wantCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPlaylistSelectionUsesCompleteEntrySession(t *testing.T) {
|
||||||
|
retry := validPlaylistRetryPolicy()
|
||||||
|
playlist := Playlist{Entries: []PlaylistEntry{
|
||||||
|
{
|
||||||
|
Video: PlaylistFeed{Domain: "video-domain", UUID: "video"},
|
||||||
|
Audio: PlaylistFeed{Domain: "audio-domain", UUID: "audio"},
|
||||||
|
SyncRequested: true,
|
||||||
|
},
|
||||||
|
{Video: PlaylistFeed{Domain: "next-domain", UUID: "next-video"}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
_, command, apply, err := ApplyPlaylistSelection(
|
||||||
|
playlist,
|
||||||
|
PlaylistState{CurrentIndex: 1, HasSelection: true},
|
||||||
|
PlaylistCommand{Kind: PlaylistSelect, Index: 0},
|
||||||
|
retry,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() error = %v", err)
|
||||||
|
}
|
||||||
|
if !apply {
|
||||||
|
t.Fatal("ApplyPlaylistSelection() apply = false, want true")
|
||||||
|
}
|
||||||
|
want := SessionConfig{
|
||||||
|
Video: FeedConfig{Domain: "video-domain", UUID: "video", Active: true},
|
||||||
|
Audio: FeedConfig{Domain: "audio-domain", UUID: "audio", Active: true},
|
||||||
|
SyncRequested: true,
|
||||||
|
Retry: retry,
|
||||||
|
}
|
||||||
|
if command.Kind != CommandSetSession || command.Session != want {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() command = %#v, want session %#v", command, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPlaylistSelectionVideoOnlyClearsAudio(t *testing.T) {
|
||||||
|
_, command, apply, err := ApplyPlaylistSelection(
|
||||||
|
navigationPlaylist(false),
|
||||||
|
PlaylistState{CurrentIndex: 1, HasSelection: true},
|
||||||
|
PlaylistCommand{Kind: PlaylistSelect, Index: 0},
|
||||||
|
validPlaylistRetryPolicy(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() error = %v", err)
|
||||||
|
}
|
||||||
|
if !apply {
|
||||||
|
t.Fatal("ApplyPlaylistSelection() apply = false, want true")
|
||||||
|
}
|
||||||
|
if command.Session.Audio != (FeedConfig{}) {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() audio = %#v, want zero value", command.Session.Audio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPlaylistSelectionApplicationDecision(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
loop bool
|
||||||
|
current int
|
||||||
|
command PlaylistCommand
|
||||||
|
want int
|
||||||
|
apply bool
|
||||||
|
}{
|
||||||
|
{name: "reselect current", current: 1, command: PlaylistCommand{Kind: PlaylistSelect, Index: 1}, want: 1, apply: true},
|
||||||
|
{name: "move next", current: 1, command: PlaylistCommand{Kind: PlaylistNext}, want: 2, apply: true},
|
||||||
|
{name: "next stops at end", current: 2, command: PlaylistCommand{Kind: PlaylistNext}, want: 2},
|
||||||
|
{name: "previous stops at beginning", current: 0, command: PlaylistCommand{Kind: PlaylistPrevious}, want: 0},
|
||||||
|
{name: "next wraps", loop: true, current: 2, command: PlaylistCommand{Kind: PlaylistNext}, want: 0, apply: true},
|
||||||
|
{name: "previous wraps", loop: true, current: 0, command: PlaylistCommand{Kind: PlaylistPrevious}, want: 2, apply: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
next, command, apply, err := ApplyPlaylistSelection(
|
||||||
|
navigationPlaylist(test.loop),
|
||||||
|
PlaylistState{CurrentIndex: test.current, HasSelection: true},
|
||||||
|
test.command,
|
||||||
|
validPlaylistRetryPolicy(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() error = %v", err)
|
||||||
|
}
|
||||||
|
wantState := PlaylistState{CurrentIndex: test.want, HasSelection: true}
|
||||||
|
if next != wantState {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() state = %#v, want %#v", next, wantState)
|
||||||
|
}
|
||||||
|
if apply != test.apply {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() apply = %v, want %v", apply, test.apply)
|
||||||
|
}
|
||||||
|
if apply && command.Kind != CommandSetSession {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() command kind = %v, want %v", command.Kind, CommandSetSession)
|
||||||
|
}
|
||||||
|
if !apply && command != (SessionCommand{}) {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() command = %#v, want zero value", command)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPlaylistSelectionErrorsDoNotApply(t *testing.T) {
|
||||||
|
current := PlaylistState{CurrentIndex: 1, HasSelection: true}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
command PlaylistCommand
|
||||||
|
retry RetryPolicy
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid command",
|
||||||
|
command: PlaylistCommand{},
|
||||||
|
retry: validPlaylistRetryPolicy(),
|
||||||
|
wantErr: ErrUnknownPlaylistCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid retry",
|
||||||
|
command: PlaylistCommand{Kind: PlaylistNext},
|
||||||
|
retry: RetryPolicy{},
|
||||||
|
wantErr: ErrInvalidRetryDelay,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
next, command, apply, err := ApplyPlaylistSelection(
|
||||||
|
navigationPlaylist(false),
|
||||||
|
current,
|
||||||
|
test.command,
|
||||||
|
test.retry,
|
||||||
|
)
|
||||||
|
if !errors.Is(err, test.wantErr) {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() error = %v, want %v", err, test.wantErr)
|
||||||
|
}
|
||||||
|
if next != current {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() state = %#v, want unchanged %#v", next, current)
|
||||||
|
}
|
||||||
|
if apply {
|
||||||
|
t.Fatal("ApplyPlaylistSelection() apply = true, want false")
|
||||||
|
}
|
||||||
|
if command != (SessionCommand{}) {
|
||||||
|
t.Fatalf("ApplyPlaylistSelection() command = %#v, want zero value", command)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validPlaylistRetryPolicy() RetryPolicy {
|
||||||
|
return RetryPolicy{
|
||||||
|
MaxAttempts: 3,
|
||||||
|
InitialDelay: time.Millisecond,
|
||||||
|
MaxDelay: time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user