session command

This commit is contained in:
Dmitry Sergeev
2026-08-31 20:22:15 +03:00
parent dcc56ac18c
commit b741b7d01f
3 changed files with 296 additions and 1 deletions
+1 -1
View File
@@ -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
+110
View File
@@ -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
}
+185
View File
@@ -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)
}
}