Refactoring #3
@@ -14,7 +14,7 @@ Size=200,200
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Connection]
|
[Window][Connection]
|
||||||
Pos=250,275
|
Pos=475,529
|
||||||
Size=640,352
|
Size=640,352
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package playback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionCommandKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
CommandSetVideo SessionCommandKind = iota + 1
|
||||||
|
CommandSetAudio
|
||||||
|
CommandStopVideo
|
||||||
|
CommandStopAudio
|
||||||
|
CommandStopAll
|
||||||
|
CommandResumeVideo
|
||||||
|
CommandResumeAudio
|
||||||
|
CommandResumeAll
|
||||||
|
CommandRemoveVideo
|
||||||
|
CommandRemoveAudio
|
||||||
|
CommandEnableSync
|
||||||
|
CommandDisableSync
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionCommand struct {
|
||||||
|
Kind SessionCommandKind
|
||||||
|
Config FeedConfig // Used only by SetVideo and SetAudio.
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnknownSessionCommand = errors.New("unknown session command")
|
||||||
|
ErrVideoNotConfigured = errors.New("video feed is not configured")
|
||||||
|
ErrAudioNotConfigured = errors.New("audio feed is not configured")
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApplySessionCommand(
|
||||||
|
current SessionConfig,
|
||||||
|
command SessionCommand,
|
||||||
|
) (SessionConfig, error) {
|
||||||
|
next := current
|
||||||
|
switch command.Kind {
|
||||||
|
case CommandSetVideo:
|
||||||
|
if !command.Config.IsConfigured() {
|
||||||
|
return current, ErrVideoNotConfigured
|
||||||
|
}
|
||||||
|
if err := command.Config.Validate(); err != nil {
|
||||||
|
return current, fmt.Errorf("validate video command: %w", err)
|
||||||
|
}
|
||||||
|
next.Video = command.Config
|
||||||
|
|
||||||
|
case CommandSetAudio:
|
||||||
|
if !command.Config.IsConfigured() {
|
||||||
|
return current, ErrAudioNotConfigured
|
||||||
|
}
|
||||||
|
if err := command.Config.Validate(); err != nil {
|
||||||
|
return current, fmt.Errorf("validate audio command: %w", err)
|
||||||
|
}
|
||||||
|
next.Audio = command.Config
|
||||||
|
|
||||||
|
case CommandStopVideo:
|
||||||
|
next.Video.Active = false
|
||||||
|
next.SyncRequested = false
|
||||||
|
|
||||||
|
case CommandStopAudio:
|
||||||
|
next.Audio.Active = false
|
||||||
|
next.SyncRequested = false
|
||||||
|
|
||||||
|
case CommandStopAll:
|
||||||
|
next.Video.Active = false
|
||||||
|
next.Audio.Active = false
|
||||||
|
|
||||||
|
case CommandResumeVideo:
|
||||||
|
if !next.Video.IsConfigured() {
|
||||||
|
return current, ErrVideoNotConfigured
|
||||||
|
}
|
||||||
|
next.Video.Active = true
|
||||||
|
|
||||||
|
case CommandResumeAudio:
|
||||||
|
if !next.Audio.IsConfigured() {
|
||||||
|
return current, ErrAudioNotConfigured
|
||||||
|
}
|
||||||
|
next.Audio.Active = true
|
||||||
|
|
||||||
|
case CommandResumeAll:
|
||||||
|
next.Video.Active = next.Video.IsConfigured()
|
||||||
|
next.Audio.Active = next.Audio.IsConfigured()
|
||||||
|
|
||||||
|
case CommandRemoveVideo:
|
||||||
|
next.Video = FeedConfig{}
|
||||||
|
next.SyncRequested = false
|
||||||
|
|
||||||
|
case CommandRemoveAudio:
|
||||||
|
next.Audio = FeedConfig{}
|
||||||
|
next.SyncRequested = false
|
||||||
|
|
||||||
|
case CommandEnableSync:
|
||||||
|
next.SyncRequested = true
|
||||||
|
|
||||||
|
case CommandDisableSync:
|
||||||
|
next.SyncRequested = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
return current, ErrUnknownSessionCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := next.Validate(); err != nil {
|
||||||
|
return current, fmt.Errorf("validate desired session: %w", err)
|
||||||
|
}
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package playback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validCommandSession() SessionConfig {
|
||||||
|
return SessionConfig{
|
||||||
|
Video: FeedConfig{Domain: "/video", UUID: "video", Active: true},
|
||||||
|
Audio: FeedConfig{Domain: "/audio", UUID: "audio", Active: true},
|
||||||
|
SyncRequested: true,
|
||||||
|
Retry: RetryPolicy{
|
||||||
|
MaxAttempts: 3,
|
||||||
|
InitialDelay: time.Millisecond,
|
||||||
|
MaxDelay: time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplySessionCommand(t *testing.T) {
|
||||||
|
base := validCommandSession()
|
||||||
|
newVideo := FeedConfig{Domain: "/new-video", UUID: "new-video", Active: false}
|
||||||
|
newAudio := FeedConfig{Domain: "/new-audio", UUID: "new-audio", Active: true}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
current SessionConfig
|
||||||
|
command SessionCommand
|
||||||
|
want SessionConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "set video replaces complete config",
|
||||||
|
current: base,
|
||||||
|
command: SessionCommand{Kind: CommandSetVideo, Config: newVideo},
|
||||||
|
want: func() SessionConfig { c := base; c.Video = newVideo; return c }(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set audio replaces complete config",
|
||||||
|
current: base,
|
||||||
|
command: SessionCommand{Kind: CommandSetAudio, Config: newAudio},
|
||||||
|
want: func() SessionConfig { c := base; c.Audio = newAudio; return c }(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stop video disables sync",
|
||||||
|
current: base,
|
||||||
|
command: SessionCommand{Kind: CommandStopVideo},
|
||||||
|
want: func() SessionConfig { c := base; c.Video.Active = false; c.SyncRequested = false; return c }(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stop audio disables sync",
|
||||||
|
current: base,
|
||||||
|
command: SessionCommand{Kind: CommandStopAudio},
|
||||||
|
want: func() SessionConfig { c := base; c.Audio.Active = false; c.SyncRequested = false; return c }(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stop all preserves sync request",
|
||||||
|
current: base,
|
||||||
|
command: SessionCommand{Kind: CommandStopAll},
|
||||||
|
want: func() SessionConfig { c := base; c.Video.Active = false; c.Audio.Active = false; return c }(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resume video",
|
||||||
|
current: func() SessionConfig { c := base; c.Video.Active = false; return c }(),
|
||||||
|
command: SessionCommand{Kind: CommandResumeVideo},
|
||||||
|
want: base,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resume audio",
|
||||||
|
current: func() SessionConfig { c := base; c.Audio.Active = false; return c }(),
|
||||||
|
command: SessionCommand{Kind: CommandResumeAudio},
|
||||||
|
want: base,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resume all activates only configured feeds",
|
||||||
|
current: func() SessionConfig { c := base; c.Video.Active = false; c.Audio = FeedConfig{}; return c }(),
|
||||||
|
command: SessionCommand{Kind: CommandResumeAll},
|
||||||
|
want: func() SessionConfig { c := base; c.Audio = FeedConfig{}; return c }(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove video clears config and disables sync",
|
||||||
|
current: base,
|
||||||
|
command: SessionCommand{Kind: CommandRemoveVideo},
|
||||||
|
want: func() SessionConfig { c := base; c.Video = FeedConfig{}; c.SyncRequested = false; return c }(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove audio clears config and disables sync",
|
||||||
|
current: base,
|
||||||
|
command: SessionCommand{Kind: CommandRemoveAudio},
|
||||||
|
want: func() SessionConfig { c := base; c.Audio = FeedConfig{}; c.SyncRequested = false; return c }(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "enable sync without feeds records request",
|
||||||
|
current: func() SessionConfig {
|
||||||
|
c := base
|
||||||
|
c.Video = FeedConfig{}
|
||||||
|
c.Audio = FeedConfig{}
|
||||||
|
c.SyncRequested = false
|
||||||
|
return c
|
||||||
|
}(),
|
||||||
|
command: SessionCommand{Kind: CommandEnableSync},
|
||||||
|
want: func() SessionConfig {
|
||||||
|
c := base
|
||||||
|
c.Video = FeedConfig{}
|
||||||
|
c.Audio = FeedConfig{}
|
||||||
|
c.SyncRequested = true
|
||||||
|
return c
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disable sync",
|
||||||
|
current: base,
|
||||||
|
command: SessionCommand{Kind: CommandDisableSync},
|
||||||
|
want: func() SessionConfig { c := base; c.SyncRequested = false; return c }(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := ApplySessionCommand(tt.current, tt.command)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplySessionCommand() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("ApplySessionCommand() = %#v, want %#v", got, tt.want)
|
||||||
|
}
|
||||||
|
if got.Retry != tt.current.Retry {
|
||||||
|
t.Fatalf("retry changed from %#v to %#v", tt.current.Retry, got.Retry)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplySessionCommandFailurePreservesState(t *testing.T) {
|
||||||
|
base := validCommandSession()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
current SessionConfig
|
||||||
|
command SessionCommand
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{"set empty video", base, SessionCommand{Kind: CommandSetVideo}, ErrVideoNotConfigured},
|
||||||
|
{"set empty audio", base, SessionCommand{Kind: CommandSetAudio}, ErrAudioNotConfigured},
|
||||||
|
{"invalid video", base, SessionCommand{Kind: CommandSetVideo, Config: FeedConfig{UUID: "video"}}, ErrFeedDomainRequired},
|
||||||
|
{"invalid audio", base, SessionCommand{Kind: CommandSetAudio, Config: FeedConfig{UUID: "audio"}}, ErrFeedDomainRequired},
|
||||||
|
{
|
||||||
|
"resume missing video",
|
||||||
|
func() SessionConfig { c := base; c.Video = FeedConfig{}; return c }(),
|
||||||
|
SessionCommand{Kind: CommandResumeVideo},
|
||||||
|
ErrVideoNotConfigured,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resume missing audio",
|
||||||
|
func() SessionConfig { c := base; c.Audio = FeedConfig{}; return c }(),
|
||||||
|
SessionCommand{Kind: CommandResumeAudio},
|
||||||
|
ErrAudioNotConfigured,
|
||||||
|
},
|
||||||
|
{"unknown command", base, SessionCommand{Kind: 255}, ErrUnknownSessionCommand},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := ApplySessionCommand(tt.current, tt.command)
|
||||||
|
if !errors.Is(err, tt.wantErr) {
|
||||||
|
t.Fatalf("ApplySessionCommand() error = %v, want %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if got != tt.current {
|
||||||
|
t.Fatalf("failed command changed state from %#v to %#v", tt.current, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplySessionCommandRejectsInvalidResult(t *testing.T) {
|
||||||
|
current := validCommandSession()
|
||||||
|
current.Retry = RetryPolicy{}
|
||||||
|
got, err := ApplySessionCommand(current, SessionCommand{Kind: CommandDisableSync})
|
||||||
|
if !errors.Is(err, ErrInvalidRetryDelay) {
|
||||||
|
t.Fatalf("ApplySessionCommand() error = %v, want %v", err, ErrInvalidRetryDelay)
|
||||||
|
}
|
||||||
|
if got != current {
|
||||||
|
t.Fatalf("failed command changed state from %#v to %#v", current, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user