architecture cleanup
This commit is contained in:
+24
-10
@@ -11,8 +11,20 @@ at any time. Synchronization is a runtime relationship between the slots, not a
|
||||
startup mode.
|
||||
|
||||
The design must leave a clean extension point for an `mxlfabrics` reader after
|
||||
the local MXL player is stable. It must also support a later playlist layer
|
||||
without moving playlist timing or selection into media readers.
|
||||
the local MXL player is stable. Playlist timing and selection must remain above
|
||||
media readers and playback workers.
|
||||
|
||||
## Current status
|
||||
|
||||
- Stages 0 through 11 are complete.
|
||||
- The reader-factory extension point from Stage 12 is complete; the
|
||||
`mxlfabrics` implementation remains future work.
|
||||
- Stage 13 is complete, including manual/timed navigation, looping,
|
||||
pause/resume, failure policies, playlist-level retry configuration, and GUI
|
||||
diagnostics.
|
||||
- Playlist lifecycle messages are explicit `PlaylistEvent` values produced by
|
||||
`PlaylistEventCoordinator`; runtime cancellation owns shutdown, so shared
|
||||
event channels are not closed by either endpoint.
|
||||
|
||||
## Required behaviour
|
||||
|
||||
@@ -61,7 +73,7 @@ without moving playlist timing or selection into media readers.
|
||||
- Stopping both members of a synchronized group closes the group atomically.
|
||||
- Stop commands cancel active reads and retry backoff promptly.
|
||||
|
||||
### Future playlist behaviour
|
||||
### Playlist behaviour
|
||||
|
||||
A playlist is an ordered list of playback entries. Each entry describes a
|
||||
complete desired session state and may contain:
|
||||
@@ -244,22 +256,24 @@ transition.
|
||||
4. Start independent workers for both configured slots.
|
||||
5. Let either worker begin playing without waiting for the other.
|
||||
|
||||
## Future playlist model
|
||||
## Playlist model
|
||||
|
||||
The exact public types can be chosen later, but the intended model is:
|
||||
The implemented model is conceptually:
|
||||
|
||||
```go
|
||||
type PlaylistEntry struct {
|
||||
Name string
|
||||
VideoUUID string
|
||||
AudioUUID string
|
||||
Video PlaylistFeed // domain + UUID, optional
|
||||
Audio PlaylistFeed // domain + UUID, optional
|
||||
SyncRequested bool
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type Playlist struct {
|
||||
Entries []PlaylistEntry
|
||||
Loop bool
|
||||
Entries []PlaylistEntry
|
||||
Loop bool
|
||||
OnFailure PlaylistFailurePolicy // wait or next
|
||||
Retry *RetryPolicy // optional playlist-wide override
|
||||
}
|
||||
```
|
||||
|
||||
@@ -505,7 +519,7 @@ Acceptance criteria:
|
||||
- Fake readers can drive all controller and supervisor tests.
|
||||
- Local MXL remains the reference implementation.
|
||||
|
||||
### Stage 13 — Simple playlist
|
||||
### Stage 13 — Simple playlist (complete)
|
||||
|
||||
Work:
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"mxl-player/internal/playback"
|
||||
)
|
||||
|
||||
const playlistReadinessInterval = 10 * time.Millisecond
|
||||
const playlistEventInterval = 10 * time.Millisecond
|
||||
|
||||
var (
|
||||
ErrPlayerPlaybackRequired = errors.New("player playback is required")
|
||||
@@ -18,10 +18,14 @@ var (
|
||||
)
|
||||
|
||||
type playerPlaylist struct {
|
||||
Controller *playback.PlaylistController
|
||||
Coordinator *playback.PlaylistReadinessCoordinator
|
||||
Commands chan playback.PlaylistCommand
|
||||
Readiness chan playback.PlaylistReadiness
|
||||
Controller *playback.PlaylistController
|
||||
|
||||
// commands is written by GUI-facing methods and consumed by Controller.
|
||||
// events is written by coordinator and consumed by Controller. Run owns
|
||||
// both goroutine lifecycles; cancellation replaces channel closing.
|
||||
coordinator *playback.PlaylistEventCoordinator
|
||||
commands chan playback.PlaylistCommand
|
||||
events chan playback.PlaylistEvent
|
||||
}
|
||||
|
||||
func newPlayerPlaylist(
|
||||
@@ -40,7 +44,7 @@ func newPlayerPlaylist(
|
||||
}
|
||||
|
||||
commands := make(chan playback.PlaylistCommand, 32)
|
||||
readiness := make(chan playback.PlaylistReadiness, 8)
|
||||
events := make(chan playback.PlaylistEvent, 8)
|
||||
controller, err := playback.NewPlaylistController(
|
||||
playlist,
|
||||
retry,
|
||||
@@ -49,12 +53,12 @@ func newPlayerPlaylist(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
coordinator, err := playback.NewPlaylistReadinessCoordinator(
|
||||
coordinator, err := playback.NewPlaylistEventCoordinator(
|
||||
controller,
|
||||
player.Controller,
|
||||
player.Status,
|
||||
readiness,
|
||||
playlistReadinessInterval,
|
||||
events,
|
||||
playlistEventInterval,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -62,9 +66,9 @@ func newPlayerPlaylist(
|
||||
|
||||
return &playerPlaylist{
|
||||
Controller: controller,
|
||||
Coordinator: coordinator,
|
||||
Commands: commands,
|
||||
Readiness: readiness,
|
||||
coordinator: coordinator,
|
||||
commands: commands,
|
||||
events: events,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -74,10 +78,10 @@ func (p *playerPlaylist) Run(ctx context.Context) error {
|
||||
|
||||
results := make(chan error, 2)
|
||||
go func() {
|
||||
results <- p.Controller.Run(runCtx, p.Commands, p.Readiness)
|
||||
results <- p.Controller.Run(runCtx, p.commands, p.events)
|
||||
}()
|
||||
go func() {
|
||||
results <- p.Coordinator.Run(runCtx)
|
||||
results <- p.coordinator.Run(runCtx)
|
||||
}()
|
||||
|
||||
first := <-results
|
||||
@@ -121,7 +125,7 @@ func (p *playerPlaylist) Resume() bool {
|
||||
|
||||
func (p *playerPlaylist) enqueue(command playback.PlaylistCommand) bool {
|
||||
select {
|
||||
case p.Commands <- command:
|
||||
case p.commands <- command:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -104,10 +104,10 @@ func TestNewPlayerPlaylistWiresComponents(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("newPlayerPlaylist() error = %v", err)
|
||||
}
|
||||
if runtime.Controller == nil || runtime.Coordinator == nil {
|
||||
if runtime.Controller == nil || runtime.coordinator == nil {
|
||||
t.Fatalf("runtime components = %#v", runtime)
|
||||
}
|
||||
if runtime.Commands == nil || runtime.Readiness == nil {
|
||||
if runtime.commands == nil || runtime.events == nil {
|
||||
t.Fatalf("runtime channels = %#v", runtime)
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func TestPlayerPlaylistNavigationHelpers(t *testing.T) {
|
||||
if !test.send() {
|
||||
t.Fatal("navigation helper returned false")
|
||||
}
|
||||
if got := <-runtime.Commands; got != test.want {
|
||||
if got := <-runtime.commands; got != test.want {
|
||||
t.Fatalf("navigation command = %#v, want %#v", got, test.want)
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func TestPlayerPlaylistNavigationQueueFull(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("newPlayerPlaylist() error = %v", err)
|
||||
}
|
||||
for range cap(runtime.Commands) {
|
||||
for range cap(runtime.commands) {
|
||||
if !runtime.Next() {
|
||||
t.Fatal("queue filled before reaching capacity")
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ Size=400,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][Settings & Info]
|
||||
Pos=1220,0
|
||||
Size=700,1080
|
||||
Pos=580,0
|
||||
Size=700,720
|
||||
Collapsed=0
|
||||
|
||||
[Window][Stats]
|
||||
|
||||
@@ -20,10 +20,6 @@ type PlaylistEvent struct {
|
||||
Failure Status
|
||||
}
|
||||
|
||||
// PlaylistReadiness is retained as an alias for callers that only publish
|
||||
// ready events. Its zero Kind is PlaylistEventReady.
|
||||
type PlaylistReadiness = PlaylistEvent
|
||||
|
||||
type playlistTimer interface {
|
||||
C() <-chan time.Time
|
||||
Stop() bool
|
||||
@@ -100,7 +96,7 @@ func NewPlaylistController(
|
||||
func (c *PlaylistController) Run(
|
||||
ctx context.Context,
|
||||
commands <-chan PlaylistCommand,
|
||||
readiness <-chan PlaylistReadiness,
|
||||
events <-chan PlaylistEvent,
|
||||
) error {
|
||||
state := PlaylistState{}
|
||||
revision := uint64(0)
|
||||
@@ -182,13 +178,13 @@ func (c *PlaylistController) Run(
|
||||
state = next
|
||||
c.publish(state, revision, timing)
|
||||
|
||||
case ready, ok := <-readiness:
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
readiness = nil
|
||||
events = nil
|
||||
continue
|
||||
}
|
||||
if ready.Kind == PlaylistEventFailed {
|
||||
if ready.Revision != revision {
|
||||
if event.Kind == PlaylistEventFailed {
|
||||
if event.Revision != revision {
|
||||
continue
|
||||
}
|
||||
stopTimer()
|
||||
@@ -197,7 +193,7 @@ func (c *PlaylistController) Run(
|
||||
EntryName: c.playlist.Entries[state.CurrentIndex].Name,
|
||||
Revision: revision,
|
||||
Policy: c.playlist.OnFailure,
|
||||
Status: ready.Failure,
|
||||
Status: event.Failure,
|
||||
})
|
||||
// A failed entry must not retain a live or apparently active
|
||||
// duration clock, even when the policy is to wait.
|
||||
@@ -228,8 +224,11 @@ func (c *PlaylistController) Run(
|
||||
c.publish(state, revision, timing)
|
||||
continue
|
||||
}
|
||||
if event.Kind != PlaylistEventReady {
|
||||
continue
|
||||
}
|
||||
if timing.Paused &&
|
||||
ready.Revision == timing.Revision &&
|
||||
event.Revision == timing.Revision &&
|
||||
timing.Duration > 0 &&
|
||||
!timing.Expired {
|
||||
timing.Ready = true
|
||||
@@ -238,7 +237,7 @@ func (c *PlaylistController) Run(
|
||||
}
|
||||
nextTiming, started := StartPlaylistTiming(
|
||||
timing,
|
||||
ready.Revision,
|
||||
event.Revision,
|
||||
c.now(),
|
||||
)
|
||||
if !started {
|
||||
|
||||
@@ -68,9 +68,11 @@ func TestPlaylistControllerStartsOneTimerForMatchingReadiness(t *testing.T) {
|
||||
return snapshot.Revision == 1
|
||||
})
|
||||
|
||||
readiness <- PlaylistReadiness{Revision: 0}
|
||||
readiness <- PlaylistEvent{Revision: 0}
|
||||
assertNoPlaylistTimer(t, timers)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1, Kind: PlaylistEventKind(99)}
|
||||
assertNoPlaylistTimer(t, timers)
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
timer := receiveFakePlaylistTimer(t, timers)
|
||||
|
||||
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
@@ -79,7 +81,7 @@ func TestPlaylistControllerStartsOneTimerForMatchingReadiness(t *testing.T) {
|
||||
if snapshot.Timing.Deadline != now.Add(10*time.Second) {
|
||||
t.Fatalf("deadline = %v, want %v", snapshot.Timing.Deadline, now.Add(10*time.Second))
|
||||
}
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
assertNoPlaylistTimer(t, timers)
|
||||
if timer.isStopped() {
|
||||
t.Fatal("timer stopped after duplicate readiness")
|
||||
@@ -101,7 +103,7 @@ func TestPlaylistControllerTimerExpiryAdvances(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
timer := receiveFakePlaylistTimer(t, timers)
|
||||
timer.fire(now.Add(10 * time.Second))
|
||||
|
||||
@@ -132,7 +134,7 @@ func TestPlaylistControllerLoopingTimerExpiryWraps(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
timer := receiveFakePlaylistTimer(t, timers)
|
||||
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Timing.Started
|
||||
@@ -160,7 +162,7 @@ func TestPlaylistControllerFinalExpiryStopsWithoutCommand(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
timer := receiveFakePlaylistTimer(t, timers)
|
||||
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Timing.Started
|
||||
@@ -192,7 +194,7 @@ func TestPlaylistControllerManualSelectionStopsOldTimer(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
oldTimer := receiveFakePlaylistTimer(t, timers)
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
|
||||
@@ -229,7 +231,7 @@ func TestPlaylistControllerZeroDurationDoesNotCreateTimer(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
assertNoPlaylistTimer(t, timers)
|
||||
|
||||
close(commands)
|
||||
@@ -244,7 +246,7 @@ func TestPlaylistControllerCancellationStopsTimer(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
timer := receiveFakePlaylistTimer(t, timers)
|
||||
cancel()
|
||||
if err := waitForPlaylistResult(t, result); !errors.Is(err, context.Canceled) {
|
||||
@@ -262,7 +264,7 @@ func TestPlaylistControllerPauseAndResumeTimer(t *testing.T) {
|
||||
|
||||
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||
_ = receivePlaylistSession(t, sessions)
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
oldTimer := receiveFakePlaylistTimer(t, timers)
|
||||
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Timing.Started
|
||||
@@ -344,7 +346,7 @@ func TestPlaylistControllerRecordsQueuedReadinessWhilePaused(t *testing.T) {
|
||||
return snapshot.Timing.Paused
|
||||
})
|
||||
|
||||
readiness <- PlaylistReadiness{Revision: 1}
|
||||
readiness <- PlaylistEvent{Revision: 1}
|
||||
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||
return snapshot.Timing.Paused && snapshot.Timing.Ready
|
||||
})
|
||||
@@ -368,7 +370,7 @@ func startTimedPlaylistController(
|
||||
) (
|
||||
*PlaylistController,
|
||||
chan PlaylistCommand,
|
||||
chan PlaylistReadiness,
|
||||
chan PlaylistEvent,
|
||||
chan SessionCommand,
|
||||
chan *fakePlaylistTimer,
|
||||
time.Time,
|
||||
@@ -390,7 +392,7 @@ func startTimedPlaylistController(
|
||||
return timer
|
||||
}
|
||||
commands := make(chan PlaylistCommand, 16)
|
||||
readiness := make(chan PlaylistReadiness, 16)
|
||||
readiness := make(chan PlaylistEvent, 16)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
result := make(chan error, 1)
|
||||
go func() { result <- controller.Run(ctx, commands, readiness) }()
|
||||
|
||||
+23
-23
@@ -18,45 +18,45 @@ type PlaybackStatusSnapshotSource interface {
|
||||
SnapshotAll() PlaybackStatusSnapshot
|
||||
}
|
||||
|
||||
type playlistReadinessTicker interface {
|
||||
type playlistEventTicker interface {
|
||||
C() <-chan time.Time
|
||||
Stop()
|
||||
}
|
||||
|
||||
type playlistReadinessTickerFactory func(time.Duration) playlistReadinessTicker
|
||||
type playlistEventTickerFactory func(time.Duration) playlistEventTicker
|
||||
|
||||
type realPlaylistReadinessTicker struct {
|
||||
type realPlaylistEventTicker struct {
|
||||
ticker *time.Ticker
|
||||
}
|
||||
|
||||
func (t realPlaylistReadinessTicker) C() <-chan time.Time { return t.ticker.C }
|
||||
func (t realPlaylistReadinessTicker) Stop() { t.ticker.Stop() }
|
||||
func (t realPlaylistEventTicker) C() <-chan time.Time { return t.ticker.C }
|
||||
func (t realPlaylistEventTicker) Stop() { t.ticker.Stop() }
|
||||
|
||||
var (
|
||||
ErrPlaylistSnapshotSourceRequired = errors.New("playlist snapshot source is required")
|
||||
ErrSessionSnapshotSourceRequired = errors.New("session snapshot source is required")
|
||||
ErrStatusSnapshotSourceRequired = errors.New("playback status snapshot source is required")
|
||||
ErrPlaylistReadinessOutputRequired = errors.New("playlist readiness output channel is required")
|
||||
ErrPlaylistReadinessInterval = errors.New("playlist readiness interval must be positive")
|
||||
ErrPlaylistSnapshotSourceRequired = errors.New("playlist snapshot source is required")
|
||||
ErrSessionSnapshotSourceRequired = errors.New("session snapshot source is required")
|
||||
ErrStatusSnapshotSourceRequired = errors.New("playback status snapshot source is required")
|
||||
ErrPlaylistEventOutputRequired = errors.New("playlist event output channel is required")
|
||||
ErrPlaylistEventInterval = errors.New("playlist event interval must be positive")
|
||||
)
|
||||
|
||||
type PlaylistReadinessCoordinator struct {
|
||||
type PlaylistEventCoordinator struct {
|
||||
playlist PlaylistSnapshotSource
|
||||
session SessionSnapshotSource
|
||||
statuses PlaybackStatusSnapshotSource
|
||||
output chan<- PlaylistReadiness
|
||||
output chan<- PlaylistEvent
|
||||
interval time.Duration
|
||||
|
||||
newTicker playlistReadinessTickerFactory
|
||||
newTicker playlistEventTickerFactory
|
||||
}
|
||||
|
||||
func NewPlaylistReadinessCoordinator(
|
||||
func NewPlaylistEventCoordinator(
|
||||
playlist PlaylistSnapshotSource,
|
||||
session SessionSnapshotSource,
|
||||
statuses PlaybackStatusSnapshotSource,
|
||||
output chan<- PlaylistReadiness,
|
||||
output chan<- PlaylistEvent,
|
||||
interval time.Duration,
|
||||
) (*PlaylistReadinessCoordinator, error) {
|
||||
) (*PlaylistEventCoordinator, error) {
|
||||
if playlist == nil {
|
||||
return nil, ErrPlaylistSnapshotSourceRequired
|
||||
}
|
||||
@@ -67,25 +67,25 @@ func NewPlaylistReadinessCoordinator(
|
||||
return nil, ErrStatusSnapshotSourceRequired
|
||||
}
|
||||
if output == nil {
|
||||
return nil, ErrPlaylistReadinessOutputRequired
|
||||
return nil, ErrPlaylistEventOutputRequired
|
||||
}
|
||||
if interval <= 0 {
|
||||
return nil, ErrPlaylistReadinessInterval
|
||||
return nil, ErrPlaylistEventInterval
|
||||
}
|
||||
|
||||
return &PlaylistReadinessCoordinator{
|
||||
return &PlaylistEventCoordinator{
|
||||
playlist: playlist,
|
||||
session: session,
|
||||
statuses: statuses,
|
||||
output: output,
|
||||
interval: interval,
|
||||
newTicker: func(interval time.Duration) playlistReadinessTicker {
|
||||
return realPlaylistReadinessTicker{ticker: time.NewTicker(interval)}
|
||||
newTicker: func(interval time.Duration) playlistEventTicker {
|
||||
return realPlaylistEventTicker{ticker: time.NewTicker(interval)}
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
|
||||
func (c *PlaylistEventCoordinator) Run(ctx context.Context) error {
|
||||
ticker := c.newTicker(c.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -139,7 +139,7 @@ func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
ready := PlaylistReadiness{Revision: playlistSnapshot.Revision}
|
||||
ready := PlaylistEvent{Revision: playlistSnapshot.Revision}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
+37
-37
@@ -63,55 +63,55 @@ func (s *fakePlaybackStatusSnapshotSource) set(snapshot PlaybackStatusSnapshot)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
type fakePlaylistReadinessTicker struct {
|
||||
type fakePlaylistEventTicker struct {
|
||||
ch chan time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
stopped bool
|
||||
}
|
||||
|
||||
func newFakePlaylistReadinessTicker() *fakePlaylistReadinessTicker {
|
||||
return &fakePlaylistReadinessTicker{ch: make(chan time.Time, 16)}
|
||||
func newFakePlaylistEventTicker() *fakePlaylistEventTicker {
|
||||
return &fakePlaylistEventTicker{ch: make(chan time.Time, 16)}
|
||||
}
|
||||
|
||||
func (t *fakePlaylistReadinessTicker) C() <-chan time.Time { return t.ch }
|
||||
func (t *fakePlaylistReadinessTicker) Stop() {
|
||||
func (t *fakePlaylistEventTicker) C() <-chan time.Time { return t.ch }
|
||||
func (t *fakePlaylistEventTicker) Stop() {
|
||||
t.mu.Lock()
|
||||
t.stopped = true
|
||||
t.mu.Unlock()
|
||||
}
|
||||
func (t *fakePlaylistReadinessTicker) tick() { t.ch <- time.Now() }
|
||||
func (t *fakePlaylistReadinessTicker) isStopped() bool {
|
||||
func (t *fakePlaylistEventTicker) tick() { t.ch <- time.Now() }
|
||||
func (t *fakePlaylistEventTicker) isStopped() bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return t.stopped
|
||||
}
|
||||
|
||||
func TestNewPlaylistReadinessCoordinatorValidatesDependencies(t *testing.T) {
|
||||
func TestNewPlaylistEventCoordinatorValidatesDependencies(t *testing.T) {
|
||||
playlist := &fakePlaylistSnapshotSource{}
|
||||
session := &fakeSessionSnapshotSource{}
|
||||
statuses := &fakePlaybackStatusSnapshotSource{}
|
||||
output := make(chan PlaylistReadiness)
|
||||
output := make(chan PlaylistEvent)
|
||||
tests := []struct {
|
||||
name string
|
||||
playlist PlaylistSnapshotSource
|
||||
session SessionSnapshotSource
|
||||
statuses PlaybackStatusSnapshotSource
|
||||
output chan<- PlaylistReadiness
|
||||
output chan<- PlaylistEvent
|
||||
interval time.Duration
|
||||
wantErr error
|
||||
}{
|
||||
{name: "playlist", session: session, statuses: statuses, output: output, interval: time.Millisecond, wantErr: ErrPlaylistSnapshotSourceRequired},
|
||||
{name: "session", playlist: playlist, statuses: statuses, output: output, interval: time.Millisecond, wantErr: ErrSessionSnapshotSourceRequired},
|
||||
{name: "statuses", playlist: playlist, session: session, output: output, interval: time.Millisecond, wantErr: ErrStatusSnapshotSourceRequired},
|
||||
{name: "output", playlist: playlist, session: session, statuses: statuses, interval: time.Millisecond, wantErr: ErrPlaylistReadinessOutputRequired},
|
||||
{name: "interval", playlist: playlist, session: session, statuses: statuses, output: output, wantErr: ErrPlaylistReadinessInterval},
|
||||
{name: "output", playlist: playlist, session: session, statuses: statuses, interval: time.Millisecond, wantErr: ErrPlaylistEventOutputRequired},
|
||||
{name: "interval", playlist: playlist, session: session, statuses: statuses, output: output, wantErr: ErrPlaylistEventInterval},
|
||||
{name: "valid", playlist: playlist, session: session, statuses: statuses, output: output, interval: time.Millisecond},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
coordinator, err := NewPlaylistReadinessCoordinator(
|
||||
coordinator, err := NewPlaylistEventCoordinator(
|
||||
test.playlist,
|
||||
test.session,
|
||||
test.statuses,
|
||||
@@ -119,7 +119,7 @@ func TestNewPlaylistReadinessCoordinatorValidatesDependencies(t *testing.T) {
|
||||
test.interval,
|
||||
)
|
||||
if !errors.Is(err, test.wantErr) {
|
||||
t.Fatalf("NewPlaylistReadinessCoordinator() error = %v, want %v", err, test.wantErr)
|
||||
t.Fatalf("NewPlaylistEventCoordinator() error = %v, want %v", err, test.wantErr)
|
||||
}
|
||||
if test.wantErr != nil && coordinator != nil {
|
||||
t.Fatalf("coordinator = %#v, want nil", coordinator)
|
||||
@@ -174,10 +174,10 @@ func TestPlaylistEntryMatchesSession(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaylistReadinessCoordinatorEmitsOncePerRevision(t *testing.T) {
|
||||
func TestPlaylistEventCoordinatorEmitsOncePerRevision(t *testing.T) {
|
||||
playlist, session, statuses := readyVideoSnapshots(1)
|
||||
output := make(chan PlaylistReadiness, 4)
|
||||
coordinator, ticker, cancel, result := startReadinessCoordinator(
|
||||
output := make(chan PlaylistEvent, 4)
|
||||
coordinator, ticker, cancel, result := startEventCoordinator(
|
||||
t,
|
||||
playlist,
|
||||
session,
|
||||
@@ -188,16 +188,16 @@ func TestPlaylistReadinessCoordinatorEmitsOncePerRevision(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
ticker.tick()
|
||||
if got := receivePlaylistReadiness(t, output); got.Revision != 1 {
|
||||
if got := receivePlaylistEvent(t, output); got.Revision != 1 {
|
||||
t.Fatalf("readiness revision = %d, want 1", got.Revision)
|
||||
}
|
||||
ticker.tick()
|
||||
assertNoPlaylistReadiness(t, output)
|
||||
assertNoPlaylistEvent(t, output)
|
||||
|
||||
next := playlistSnapshotForVideo(2)
|
||||
playlist.set(next, true)
|
||||
ticker.tick()
|
||||
if got := receivePlaylistReadiness(t, output); got.Revision != 2 {
|
||||
if got := receivePlaylistEvent(t, output); got.Revision != 2 {
|
||||
t.Fatalf("readiness revision = %d, want 2", got.Revision)
|
||||
}
|
||||
|
||||
@@ -210,10 +210,10 @@ func TestPlaylistReadinessCoordinatorEmitsOncePerRevision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
|
||||
func TestPlaylistEventCoordinatorWaitsForAllConditions(t *testing.T) {
|
||||
playlist, session, statuses := readyVideoSnapshots(1)
|
||||
output := make(chan PlaylistReadiness, 1)
|
||||
_, ticker, cancel, result := startReadinessCoordinator(t, playlist, session, statuses, output)
|
||||
output := make(chan PlaylistEvent, 1)
|
||||
_, ticker, cancel, result := startEventCoordinator(t, playlist, session, statuses, output)
|
||||
defer cancel()
|
||||
|
||||
tests := []struct {
|
||||
@@ -256,7 +256,7 @@ func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
|
||||
statuses.set(validStatuses.snapshot)
|
||||
test.mutate()
|
||||
ticker.tick()
|
||||
assertNoPlaylistReadiness(t, output)
|
||||
assertNoPlaylistEvent(t, output)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -264,10 +264,10 @@ func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
|
||||
_ = waitForPlaylistResult(t, result)
|
||||
}
|
||||
|
||||
func TestPlaylistReadinessCoordinatorCancellationWhileBlockedSending(t *testing.T) {
|
||||
func TestPlaylistEventCoordinatorCancellationWhileBlockedSending(t *testing.T) {
|
||||
playlist, session, statuses := readyVideoSnapshots(1)
|
||||
output := make(chan PlaylistReadiness)
|
||||
_, ticker, cancel, result := startReadinessCoordinator(t, playlist, session, statuses, output)
|
||||
output := make(chan PlaylistEvent)
|
||||
_, ticker, cancel, result := startEventCoordinator(t, playlist, session, statuses, output)
|
||||
|
||||
ticker.tick()
|
||||
time.Sleep(time.Millisecond)
|
||||
@@ -322,15 +322,15 @@ func playlistSnapshotForVideo(revision uint64) PlaylistSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
func startReadinessCoordinator(
|
||||
func startEventCoordinator(
|
||||
t *testing.T,
|
||||
playlist PlaylistSnapshotSource,
|
||||
session SessionSnapshotSource,
|
||||
statuses PlaybackStatusSnapshotSource,
|
||||
output chan<- PlaylistReadiness,
|
||||
) (*PlaylistReadinessCoordinator, *fakePlaylistReadinessTicker, context.CancelFunc, <-chan error) {
|
||||
output chan<- PlaylistEvent,
|
||||
) (*PlaylistEventCoordinator, *fakePlaylistEventTicker, context.CancelFunc, <-chan error) {
|
||||
t.Helper()
|
||||
coordinator, err := NewPlaylistReadinessCoordinator(
|
||||
coordinator, err := NewPlaylistEventCoordinator(
|
||||
playlist,
|
||||
session,
|
||||
statuses,
|
||||
@@ -338,28 +338,28 @@ func startReadinessCoordinator(
|
||||
time.Millisecond,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPlaylistReadinessCoordinator() error = %v", err)
|
||||
t.Fatalf("NewPlaylistEventCoordinator() error = %v", err)
|
||||
}
|
||||
ticker := newFakePlaylistReadinessTicker()
|
||||
coordinator.newTicker = func(time.Duration) playlistReadinessTicker { return ticker }
|
||||
ticker := newFakePlaylistEventTicker()
|
||||
coordinator.newTicker = func(time.Duration) playlistEventTicker { return ticker }
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
result := make(chan error, 1)
|
||||
go func() { result <- coordinator.Run(ctx) }()
|
||||
return coordinator, ticker, cancel, result
|
||||
}
|
||||
|
||||
func receivePlaylistReadiness(t *testing.T, output <-chan PlaylistReadiness) PlaylistReadiness {
|
||||
func receivePlaylistEvent(t *testing.T, output <-chan PlaylistEvent) PlaylistEvent {
|
||||
t.Helper()
|
||||
select {
|
||||
case readiness := <-output:
|
||||
return readiness
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for playlist readiness")
|
||||
return PlaylistReadiness{}
|
||||
return PlaylistEvent{}
|
||||
}
|
||||
}
|
||||
|
||||
func assertNoPlaylistReadiness(t *testing.T, output <-chan PlaylistReadiness) {
|
||||
func assertNoPlaylistEvent(t *testing.T, output <-chan PlaylistEvent) {
|
||||
t.Helper()
|
||||
select {
|
||||
case readiness := <-output:
|
||||
@@ -146,7 +146,7 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
|
||||
t.Fatalf("NewPlaylistController() error = %v", err)
|
||||
}
|
||||
commands := make(chan PlaylistCommand, 2)
|
||||
events := make(chan PlaylistReadiness, 2)
|
||||
events := make(chan PlaylistEvent, 2)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
result := make(chan error, 1)
|
||||
go func() { result <- controller.Run(ctx, commands, events) }()
|
||||
|
||||
@@ -116,7 +116,7 @@ type playlistPipelineHarness struct {
|
||||
sessions chan SessionCommand
|
||||
session *fakeSessionSnapshotSource
|
||||
statuses *fakePlaybackStatusSnapshotSource
|
||||
ticker *fakePlaylistReadinessTicker
|
||||
ticker *fakePlaylistEventTicker
|
||||
cancel context.CancelFunc
|
||||
results chan error
|
||||
}
|
||||
@@ -128,17 +128,17 @@ func startPlaylistPipeline(t *testing.T, playlist Playlist) *playlistPipelineHar
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
events := make(chan PlaylistReadiness, 8)
|
||||
events := make(chan PlaylistEvent, 8)
|
||||
session := &fakeSessionSnapshotSource{}
|
||||
statuses := &fakePlaybackStatusSnapshotSource{}
|
||||
coordinator, err := NewPlaylistReadinessCoordinator(
|
||||
coordinator, err := NewPlaylistEventCoordinator(
|
||||
controller, session, statuses, events, time.Millisecond,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ticker := newFakePlaylistReadinessTicker()
|
||||
coordinator.newTicker = func(time.Duration) playlistReadinessTicker { return ticker }
|
||||
ticker := newFakePlaylistEventTicker()
|
||||
coordinator.newTicker = func(time.Duration) playlistEventTicker { return ticker }
|
||||
commands := make(chan PlaylistCommand, 8)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
results := make(chan error, 2)
|
||||
|
||||
Reference in New Issue
Block a user