Refactoring #3

Merged
itten merged 87 commits from refactoring into main 2026-09-01 23:52:36 +03:00
2 changed files with 284 additions and 0 deletions
Showing only changes of commit a79a2d9c7a - Show all commits
+93
View File
@@ -0,0 +1,93 @@
package playback
import "errors"
type PlaylistCommandKind uint8
const (
PlaylistSelect PlaylistCommandKind = iota + 1
PlaylistNext
PlaylistPrevious
)
type PlaylistCommand struct {
Kind PlaylistCommandKind
Index int
}
type PlaylistState struct {
CurrentIndex int
HasSelection bool
}
var (
ErrPlaylistEmpty = errors.New("playlist is empty")
ErrPlaylistIndexOutOfRange = errors.New("playlist index is out of range")
ErrUnknownPlaylistCommand = errors.New("unknown playlist command")
)
func ApplyPlaylistCommand(
playlist Playlist,
current PlaylistState,
command PlaylistCommand,
) (PlaylistState, error) {
if err := playlist.Validate(); err != nil {
return current, err
}
if len(playlist.Entries) == 0 {
return current, ErrPlaylistEmpty
}
lastIndex := len(playlist.Entries) - 1
switch command.Kind {
case PlaylistSelect:
if command.Index < 0 || command.Index > lastIndex {
return current, ErrPlaylistIndexOutOfRange
}
return PlaylistState{CurrentIndex: command.Index, HasSelection: true}, nil
case PlaylistNext:
if !current.HasSelection {
return PlaylistState{CurrentIndex: 0, HasSelection: true}, nil
}
if current.CurrentIndex < 0 || current.CurrentIndex > lastIndex {
return current, ErrPlaylistIndexOutOfRange
}
if current.CurrentIndex == lastIndex {
if playlist.Loop {
return PlaylistState{CurrentIndex: 0, HasSelection: true}, nil
}
return current, nil
}
return PlaylistState{CurrentIndex: current.CurrentIndex + 1, HasSelection: true}, nil
case PlaylistPrevious:
if !current.HasSelection {
index := 0
if playlist.Loop {
index = lastIndex
}
return PlaylistState{CurrentIndex: index, HasSelection: true}, nil
}
if current.CurrentIndex < 0 || current.CurrentIndex > lastIndex {
return current, ErrPlaylistIndexOutOfRange
}
if current.CurrentIndex == 0 {
if playlist.Loop {
return PlaylistState{CurrentIndex: lastIndex, HasSelection: true}, nil
}
return current, nil
}
return PlaylistState{CurrentIndex: current.CurrentIndex - 1, HasSelection: true}, nil
default:
return current, ErrUnknownPlaylistCommand
}
}
func (s PlaylistState) Entry(playlist Playlist) (PlaylistEntry, bool) {
if !s.HasSelection || s.CurrentIndex < 0 || s.CurrentIndex >= len(playlist.Entries) {
return PlaylistEntry{}, false
}
return playlist.Entries[s.CurrentIndex], true
}
@@ -0,0 +1,191 @@
package playback
import (
"errors"
"testing"
)
func navigationPlaylist(loop bool) Playlist {
return Playlist{
Entries: []PlaylistEntry{
{Name: "first", Video: PlaylistFeed{Domain: "domain", UUID: "video-1"}},
{Name: "second", Audio: PlaylistFeed{Domain: "domain", UUID: "audio-2"}},
{Name: "third", Video: PlaylistFeed{Domain: "domain", UUID: "video-3"}},
},
Loop: loop,
}
}
func TestApplyPlaylistCommandSelect(t *testing.T) {
current := PlaylistState{CurrentIndex: 1, HasSelection: true}
got, err := ApplyPlaylistCommand(
navigationPlaylist(false),
current,
PlaylistCommand{Kind: PlaylistSelect, Index: 2},
)
if err != nil {
t.Fatalf("ApplyPlaylistCommand() error = %v", err)
}
want := PlaylistState{CurrentIndex: 2, HasSelection: true}
if got != want {
t.Fatalf("ApplyPlaylistCommand() = %#v, want %#v", got, want)
}
}
func TestApplyPlaylistCommandWithoutSelection(t *testing.T) {
tests := []struct {
name string
loop bool
command PlaylistCommandKind
want int
}{
{name: "next", command: PlaylistNext, want: 0},
{name: "previous without loop", command: PlaylistPrevious, want: 0},
{name: "previous with loop", loop: true, command: PlaylistPrevious, want: 2},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := ApplyPlaylistCommand(
navigationPlaylist(test.loop),
PlaylistState{},
PlaylistCommand{Kind: test.command},
)
if err != nil {
t.Fatalf("ApplyPlaylistCommand() error = %v", err)
}
want := PlaylistState{CurrentIndex: test.want, HasSelection: true}
if got != want {
t.Fatalf("ApplyPlaylistCommand() = %#v, want %#v", got, want)
}
})
}
}
func TestApplyPlaylistCommandNavigation(t *testing.T) {
tests := []struct {
name string
loop bool
current int
command PlaylistCommandKind
want int
}{
{name: "next", current: 1, command: PlaylistNext, want: 2},
{name: "previous", current: 1, command: PlaylistPrevious, want: 0},
{name: "next stops at end", current: 2, command: PlaylistNext, want: 2},
{name: "previous stops at beginning", current: 0, command: PlaylistPrevious, want: 0},
{name: "next wraps", loop: true, current: 2, command: PlaylistNext, want: 0},
{name: "previous wraps", loop: true, current: 0, command: PlaylistPrevious, want: 2},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
current := PlaylistState{CurrentIndex: test.current, HasSelection: true}
got, err := ApplyPlaylistCommand(
navigationPlaylist(test.loop),
current,
PlaylistCommand{Kind: test.command},
)
if err != nil {
t.Fatalf("ApplyPlaylistCommand() error = %v", err)
}
want := PlaylistState{CurrentIndex: test.want, HasSelection: true}
if got != want {
t.Fatalf("ApplyPlaylistCommand() = %#v, want %#v", got, want)
}
})
}
}
func TestApplyPlaylistCommandErrorsLeaveStateUnchanged(t *testing.T) {
current := PlaylistState{CurrentIndex: 1, HasSelection: true}
tests := []struct {
name string
playlist Playlist
command PlaylistCommand
wantErr error
}{
{
name: "empty playlist",
playlist: Playlist{},
command: PlaylistCommand{Kind: PlaylistNext},
wantErr: ErrPlaylistEmpty,
},
{
name: "negative selection",
playlist: navigationPlaylist(false),
command: PlaylistCommand{Kind: PlaylistSelect, Index: -1},
wantErr: ErrPlaylistIndexOutOfRange,
},
{
name: "selection past end",
playlist: navigationPlaylist(false),
command: PlaylistCommand{Kind: PlaylistSelect, Index: 3},
wantErr: ErrPlaylistIndexOutOfRange,
},
{
name: "stale current index",
playlist: navigationPlaylist(false),
command: PlaylistCommand{Kind: PlaylistNext},
wantErr: ErrPlaylistIndexOutOfRange,
},
{
name: "unknown command",
playlist: navigationPlaylist(false),
command: PlaylistCommand{},
wantErr: ErrUnknownPlaylistCommand,
},
{
name: "invalid playlist",
playlist: Playlist{Entries: []PlaylistEntry{
{},
}},
command: PlaylistCommand{Kind: PlaylistNext},
wantErr: ErrPlaylistEntryEmpty,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
state := current
if test.name == "stale current index" {
state.CurrentIndex = 4
}
got, err := ApplyPlaylistCommand(test.playlist, state, test.command)
if !errors.Is(err, test.wantErr) {
t.Fatalf("ApplyPlaylistCommand() error = %v, want %v", err, test.wantErr)
}
if got != state {
t.Fatalf("ApplyPlaylistCommand() = %#v, want unchanged %#v", got, state)
}
})
}
}
func TestPlaylistStateEntry(t *testing.T) {
playlist := navigationPlaylist(false)
tests := []struct {
name string
state PlaylistState
want string
ok bool
}{
{name: "no selection", state: PlaylistState{}},
{name: "selected", state: PlaylistState{CurrentIndex: 1, HasSelection: true}, want: "second", ok: true},
{name: "negative stale index", state: PlaylistState{CurrentIndex: -1, HasSelection: true}},
{name: "stale index", state: PlaylistState{CurrentIndex: 3, HasSelection: true}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
entry, ok := test.state.Entry(playlist)
if ok != test.ok {
t.Fatalf("Entry() ok = %v, want %v", ok, test.ok)
}
if entry.Name != test.want {
t.Fatalf("Entry() name = %q, want %q", entry.Name, test.want)
}
})
}
}