session plan
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package playback
|
||||
|
||||
import "fmt"
|
||||
|
||||
type SessionTopology uint8
|
||||
|
||||
const (
|
||||
TopologyIdle SessionTopology = iota
|
||||
TopologyIndependent
|
||||
TopologySynchronized
|
||||
)
|
||||
|
||||
type SyncPredicate func(video, audio FeedConfig) bool
|
||||
|
||||
type SessionPlan struct {
|
||||
Topology SessionTopology
|
||||
|
||||
// Desired states for independent slots.
|
||||
Video FeedConfig
|
||||
Audio FeedConfig
|
||||
|
||||
// Desired state for the synchronized slot.
|
||||
Sync SyncPairConfig
|
||||
}
|
||||
|
||||
func BuildSessionPlan(
|
||||
desired SessionConfig,
|
||||
canSync SyncPredicate,
|
||||
) (SessionPlan, error) {
|
||||
if err := desired.Validate(); err != nil {
|
||||
return SessionPlan{}, fmt.Errorf("validate desired session: %w", err)
|
||||
}
|
||||
|
||||
if desired.SyncRequested &&
|
||||
desired.Video.Active &&
|
||||
desired.Audio.Active &&
|
||||
canSync != nil &&
|
||||
canSync(desired.Video, desired.Audio) {
|
||||
return SessionPlan{
|
||||
Topology: TopologySynchronized,
|
||||
Video: stoppedFeed(desired.Video),
|
||||
Audio: stoppedFeed(desired.Audio),
|
||||
Sync: SyncPairConfig{
|
||||
Video: desired.Video,
|
||||
Audio: desired.Audio,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if desired.Video.Active || desired.Audio.Active {
|
||||
return SessionPlan{
|
||||
Topology: TopologyIndependent,
|
||||
Video: desired.Video,
|
||||
Audio: desired.Audio,
|
||||
Sync: SyncPairConfig{
|
||||
Video: stoppedFeed(desired.Video),
|
||||
Audio: stoppedFeed(desired.Audio),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return SessionPlan{
|
||||
Topology: TopologyIdle,
|
||||
Video: stoppedFeed(desired.Video),
|
||||
Audio: stoppedFeed(desired.Audio),
|
||||
Sync: SyncPairConfig{
|
||||
Video: stoppedFeed(desired.Video),
|
||||
Audio: stoppedFeed(desired.Audio),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func stoppedFeed(config FeedConfig) FeedConfig {
|
||||
config.Active = false
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package playback
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildSessionPlan(t *testing.T) {
|
||||
base := validCommandSession()
|
||||
stoppedVideo := stoppedFeed(base.Video)
|
||||
stoppedAudio := stoppedFeed(base.Audio)
|
||||
independent := func(video, audio FeedConfig) SessionPlan {
|
||||
return SessionPlan{
|
||||
Topology: TopologyIndependent,
|
||||
Video: video,
|
||||
Audio: audio,
|
||||
Sync: SyncPairConfig{Video: stoppedFeed(video), Audio: stoppedFeed(audio)},
|
||||
}
|
||||
}
|
||||
idle := func(video, audio FeedConfig) SessionPlan {
|
||||
return SessionPlan{
|
||||
Topology: TopologyIdle,
|
||||
Video: stoppedFeed(video),
|
||||
Audio: stoppedFeed(audio),
|
||||
Sync: SyncPairConfig{Video: stoppedFeed(video), Audio: stoppedFeed(audio)},
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
desired SessionConfig
|
||||
canSync SyncPredicate
|
||||
want SessionPlan
|
||||
}{
|
||||
{
|
||||
name: "both active with sync disabled stay independent",
|
||||
desired: func() SessionConfig { c := base; c.SyncRequested = false; return c }(),
|
||||
canSync: func(FeedConfig, FeedConfig) bool { return true },
|
||||
want: independent(base.Video, base.Audio),
|
||||
},
|
||||
{
|
||||
name: "eligible requested pair is synchronized",
|
||||
desired: base,
|
||||
canSync: func(FeedConfig, FeedConfig) bool { return true },
|
||||
want: SessionPlan{
|
||||
Topology: TopologySynchronized,
|
||||
Video: stoppedVideo,
|
||||
Audio: stoppedAudio,
|
||||
Sync: SyncPairConfig{Video: base.Video, Audio: base.Audio},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ineligible requested pair remains independent",
|
||||
desired: base,
|
||||
canSync: func(FeedConfig, FeedConfig) bool { return false },
|
||||
want: independent(base.Video, base.Audio),
|
||||
},
|
||||
{
|
||||
name: "nil capability remains independent",
|
||||
desired: base,
|
||||
want: independent(base.Video, base.Audio),
|
||||
},
|
||||
{
|
||||
name: "video only",
|
||||
desired: func() SessionConfig { c := base; c.Audio.Active = false; return c }(),
|
||||
canSync: func(FeedConfig, FeedConfig) bool { return true },
|
||||
want: independent(base.Video, stoppedAudio),
|
||||
},
|
||||
{
|
||||
name: "audio only",
|
||||
desired: func() SessionConfig { c := base; c.Video.Active = false; return c }(),
|
||||
canSync: func(FeedConfig, FeedConfig) bool { return true },
|
||||
want: independent(stoppedVideo, base.Audio),
|
||||
},
|
||||
{
|
||||
name: "stopped feeds are idle and retain configuration",
|
||||
desired: func() SessionConfig { c := base; c.Video.Active = false; c.Audio.Active = false; return c }(),
|
||||
canSync: func(FeedConfig, FeedConfig) bool { return true },
|
||||
want: idle(stoppedVideo, stoppedAudio),
|
||||
},
|
||||
{
|
||||
name: "empty session is idle",
|
||||
desired: func() SessionConfig { c := base; c.Video = FeedConfig{}; c.Audio = FeedConfig{}; return c }(),
|
||||
canSync: func(FeedConfig, FeedConfig) bool { return true },
|
||||
want: idle(FeedConfig{}, FeedConfig{}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := BuildSessionPlan(tt.desired, tt.canSync)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSessionPlan() error = %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("BuildSessionPlan() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSessionPlanRejectsInvalidSession(t *testing.T) {
|
||||
desired := validCommandSession()
|
||||
desired.Video = FeedConfig{UUID: "video", Active: true}
|
||||
got, err := BuildSessionPlan(desired, nil)
|
||||
if !errors.Is(err, ErrFeedDomainRequired) {
|
||||
t.Fatalf("BuildSessionPlan() error = %v, want %v", err, ErrFeedDomainRequired)
|
||||
}
|
||||
if got != (SessionPlan{}) {
|
||||
t.Fatalf("BuildSessionPlan() = %#v, want zero plan", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSessionPlanPassesCompleteFeedsToPredicate(t *testing.T) {
|
||||
desired := validCommandSession()
|
||||
called := false
|
||||
_, err := BuildSessionPlan(desired, func(video, audio FeedConfig) bool {
|
||||
called = true
|
||||
if video != desired.Video || audio != desired.Audio {
|
||||
t.Fatalf("predicate feeds = %#v %#v", video, audio)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !called {
|
||||
t.Fatal("sync predicate was not called")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user