session plan
This commit is contained in:
@@ -14,7 +14,7 @@ Size=200,200
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Connection]
|
[Window][Connection]
|
||||||
Pos=475,529
|
Pos=628,539
|
||||||
Size=640,352
|
Size=613,164
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
|
|||||||
@@ -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