Refactoring #3
@@ -0,0 +1,52 @@
|
||||
package playback
|
||||
|
||||
func IsSessionPlaying(
|
||||
session SessionSnapshot,
|
||||
statuses PlaybackStatusSnapshot,
|
||||
) bool {
|
||||
if statuses.Generation != session.Generation {
|
||||
return false
|
||||
}
|
||||
|
||||
switch session.Plan.Topology {
|
||||
case TopologyIndependent:
|
||||
hasActiveFeed := session.Plan.Video.Active || session.Plan.Audio.Active
|
||||
if !hasActiveFeed {
|
||||
return false
|
||||
}
|
||||
if session.Plan.Video.Active && !statusIsPlaying(
|
||||
statuses.Video,
|
||||
statuses.HasVideo,
|
||||
session.Generation,
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if session.Plan.Audio.Active && !statusIsPlaying(
|
||||
statuses.Audio,
|
||||
statuses.HasAudio,
|
||||
session.Generation,
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
case TopologySynchronized:
|
||||
return statusIsPlaying(
|
||||
statuses.Sync,
|
||||
statuses.HasSync,
|
||||
session.Generation,
|
||||
)
|
||||
|
||||
case TopologyIdle:
|
||||
return false
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func statusIsPlaying(status Status, present bool, generation uint64) bool {
|
||||
return present &&
|
||||
status.Generation == generation &&
|
||||
status.State == StatePlaying
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package playback
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsSessionPlaying(t *testing.T) {
|
||||
const generation = 4
|
||||
playing := func(unit Unit) Status {
|
||||
return Status{Unit: unit, State: StatePlaying, Generation: generation}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
session SessionSnapshot
|
||||
statuses PlaybackStatusSnapshot
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "video only playing",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{
|
||||
Topology: TopologyIndependent,
|
||||
Video: FeedConfig{UUID: "video", Active: true},
|
||||
},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{
|
||||
Generation: generation,
|
||||
Video: playing(UnitVideo),
|
||||
HasVideo: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "audio only playing",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{
|
||||
Topology: TopologyIndependent,
|
||||
Audio: FeedConfig{UUID: "audio", Active: true},
|
||||
},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{
|
||||
Generation: generation,
|
||||
Audio: playing(UnitAudio),
|
||||
HasAudio: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "both independent feeds playing",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{
|
||||
Topology: TopologyIndependent,
|
||||
Video: FeedConfig{UUID: "video", Active: true},
|
||||
Audio: FeedConfig{UUID: "audio", Active: true},
|
||||
},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{
|
||||
Generation: generation,
|
||||
Video: playing(UnitVideo),
|
||||
HasVideo: true,
|
||||
Audio: playing(UnitAudio),
|
||||
HasAudio: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "only video of independent pair playing",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{
|
||||
Topology: TopologyIndependent,
|
||||
Video: FeedConfig{UUID: "video", Active: true},
|
||||
Audio: FeedConfig{UUID: "audio", Active: true},
|
||||
},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{
|
||||
Generation: generation,
|
||||
Video: playing(UnitVideo),
|
||||
HasVideo: true,
|
||||
Audio: Status{Unit: UnitAudio, State: StateConnecting, Generation: generation},
|
||||
HasAudio: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "synchronized unit playing",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{Topology: TopologySynchronized},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{
|
||||
Generation: generation,
|
||||
Sync: playing(UnitSync),
|
||||
HasSync: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "sync ignores independent playing statuses",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{Topology: TopologySynchronized},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{
|
||||
Generation: generation,
|
||||
Video: playing(UnitVideo),
|
||||
HasVideo: true,
|
||||
Audio: playing(UnitAudio),
|
||||
HasAudio: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reconnecting is not playing",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{
|
||||
Topology: TopologyIndependent,
|
||||
Video: FeedConfig{UUID: "video", Active: true},
|
||||
},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{
|
||||
Generation: generation,
|
||||
Video: Status{Unit: UnitVideo, State: StateReconnecting, Generation: generation},
|
||||
HasVideo: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing status",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{
|
||||
Topology: TopologyIndependent,
|
||||
Video: FeedConfig{UUID: "video", Active: true},
|
||||
},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{Generation: generation},
|
||||
},
|
||||
{
|
||||
name: "older status snapshot",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{Topology: TopologySynchronized},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{
|
||||
Generation: generation - 1,
|
||||
Sync: Status{Unit: UnitSync, State: StatePlaying, Generation: generation - 1},
|
||||
HasSync: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "newer status snapshot",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{Topology: TopologySynchronized},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{
|
||||
Generation: generation + 1,
|
||||
Sync: Status{Unit: UnitSync, State: StatePlaying, Generation: generation + 1},
|
||||
HasSync: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "individual status has wrong generation",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{Topology: TopologySynchronized},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{
|
||||
Generation: generation,
|
||||
Sync: Status{Unit: UnitSync, State: StatePlaying, Generation: generation - 1},
|
||||
HasSync: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idle",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{Topology: TopologyIdle},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{Generation: generation},
|
||||
},
|
||||
{
|
||||
name: "unknown topology",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{Topology: SessionTopology(255)},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{Generation: generation},
|
||||
},
|
||||
{
|
||||
name: "independent without active feeds",
|
||||
session: SessionSnapshot{
|
||||
Generation: generation,
|
||||
Plan: SessionPlan{Topology: TopologyIndependent},
|
||||
},
|
||||
statuses: PlaybackStatusSnapshot{Generation: generation},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := IsSessionPlaying(test.session, test.statuses); got != test.want {
|
||||
t.Fatalf("IsSessionPlaying() = %v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,19 @@ type StatusStore struct {
|
||||
statuses map[Unit]Status
|
||||
}
|
||||
|
||||
type PlaybackStatusSnapshot struct {
|
||||
Generation uint64
|
||||
|
||||
Video Status
|
||||
HasVideo bool
|
||||
|
||||
Audio Status
|
||||
HasAudio bool
|
||||
|
||||
Sync Status
|
||||
HasSync bool
|
||||
}
|
||||
|
||||
func NewStatusStore() *StatusStore {
|
||||
return &StatusStore{
|
||||
statuses: make(map[Unit]Status),
|
||||
@@ -34,6 +47,17 @@ func (s *StatusStore) Snapshot(unit Unit) (Status, bool) {
|
||||
return status, ok
|
||||
}
|
||||
|
||||
func (s *StatusStore) SnapshotAll() PlaybackStatusSnapshot {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
snapshot := PlaybackStatusSnapshot{Generation: s.generation}
|
||||
snapshot.Video, snapshot.HasVideo = s.statuses[UnitVideo]
|
||||
snapshot.Audio, snapshot.HasAudio = s.statuses[UnitAudio]
|
||||
snapshot.Sync, snapshot.HasSync = s.statuses[UnitSync]
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func (s *StatusStore) Clear(unit Unit) {
|
||||
s.mu.Lock()
|
||||
delete(s.statuses, unit)
|
||||
|
||||
@@ -153,3 +153,69 @@ func TestStatusStoreKeepsEqualGenerationUnitsIndependent(t *testing.T) {
|
||||
t.Fatalf("audio Snapshot() = %#v, %t", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusStoreSnapshotAll(t *testing.T) {
|
||||
store := NewStatusStore()
|
||||
wantVideo := Status{Unit: UnitVideo, State: StatePlaying, Generation: 5}
|
||||
wantAudio := Status{Unit: UnitAudio, State: StateReconnecting, Generation: 5}
|
||||
store.Observe(wantVideo)
|
||||
store.Observe(wantAudio)
|
||||
|
||||
got := store.SnapshotAll()
|
||||
if got.Generation != 5 {
|
||||
t.Fatalf("SnapshotAll() generation = %d, want 5", got.Generation)
|
||||
}
|
||||
if !got.HasVideo || got.Video != wantVideo {
|
||||
t.Fatalf("SnapshotAll() video = %#v, %v; want %#v, true", got.Video, got.HasVideo, wantVideo)
|
||||
}
|
||||
if !got.HasAudio || got.Audio != wantAudio {
|
||||
t.Fatalf("SnapshotAll() audio = %#v, %v; want %#v, true", got.Audio, got.HasAudio, wantAudio)
|
||||
}
|
||||
if got.HasSync {
|
||||
t.Fatalf("SnapshotAll() HasSync = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusStoreSnapshotAllClearsOldGenerationUnits(t *testing.T) {
|
||||
store := NewStatusStore()
|
||||
store.Observe(Status{Unit: UnitVideo, State: StatePlaying, Generation: 2})
|
||||
store.Observe(Status{Unit: UnitAudio, State: StatePlaying, Generation: 2})
|
||||
wantSync := Status{Unit: UnitSync, State: StateConnecting, Generation: 3}
|
||||
store.Observe(wantSync)
|
||||
|
||||
got := store.SnapshotAll()
|
||||
if got.Generation != 3 {
|
||||
t.Fatalf("SnapshotAll() generation = %d, want 3", got.Generation)
|
||||
}
|
||||
if got.HasVideo || got.HasAudio {
|
||||
t.Fatalf("SnapshotAll() retained old units: %#v", got)
|
||||
}
|
||||
if !got.HasSync || got.Sync != wantSync {
|
||||
t.Fatalf("SnapshotAll() sync = %#v, %v; want %#v, true", got.Sync, got.HasSync, wantSync)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusStoreSnapshotAllConcurrentObserve(t *testing.T) {
|
||||
store := NewStatusStore()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for generation := uint64(1); generation <= 1000; generation++ {
|
||||
store.Observe(Status{
|
||||
Unit: Unit(generation % 3),
|
||||
State: StatePlaying,
|
||||
Generation: generation,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
_ = store.SnapshotAll()
|
||||
return
|
||||
default:
|
||||
_ = store.SnapshotAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user