Cleanout #4

Merged
itten merged 7 commits from cleanout into main 2026-09-02 00:46:14 +03:00
10 changed files with 141 additions and 122 deletions
Showing only changes of commit e7b032ce77 - Show all commits
+22 -8
View File
@@ -11,8 +11,20 @@ at any time. Synchronization is a runtime relationship between the slots, not a
startup mode. startup mode.
The design must leave a clean extension point for an `mxlfabrics` reader after 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 the local MXL player is stable. Playlist timing and selection must remain above
without moving playlist timing or selection into media readers. 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 ## 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. - Stopping both members of a synchronized group closes the group atomically.
- Stop commands cancel active reads and retry backoff promptly. - 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 A playlist is an ordered list of playback entries. Each entry describes a
complete desired session state and may contain: complete desired session state and may contain:
@@ -244,15 +256,15 @@ transition.
4. Start independent workers for both configured slots. 4. Start independent workers for both configured slots.
5. Let either worker begin playing without waiting for the other. 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 ```go
type PlaylistEntry struct { type PlaylistEntry struct {
Name string Name string
VideoUUID string Video PlaylistFeed // domain + UUID, optional
AudioUUID string Audio PlaylistFeed // domain + UUID, optional
SyncRequested bool SyncRequested bool
Duration time.Duration Duration time.Duration
} }
@@ -260,6 +272,8 @@ type PlaylistEntry struct {
type Playlist struct { type Playlist struct {
Entries []PlaylistEntry Entries []PlaylistEntry
Loop bool 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. - Fake readers can drive all controller and supervisor tests.
- Local MXL remains the reference implementation. - Local MXL remains the reference implementation.
### Stage 13 — Simple playlist ### Stage 13 — Simple playlist (complete)
Work: Work:
+18 -14
View File
@@ -9,7 +9,7 @@ import (
"mxl-player/internal/playback" "mxl-player/internal/playback"
) )
const playlistReadinessInterval = 10 * time.Millisecond const playlistEventInterval = 10 * time.Millisecond
var ( var (
ErrPlayerPlaybackRequired = errors.New("player playback is required") ErrPlayerPlaybackRequired = errors.New("player playback is required")
@@ -19,9 +19,13 @@ var (
type playerPlaylist struct { type playerPlaylist struct {
Controller *playback.PlaylistController Controller *playback.PlaylistController
Coordinator *playback.PlaylistReadinessCoordinator
Commands chan playback.PlaylistCommand // commands is written by GUI-facing methods and consumed by Controller.
Readiness chan playback.PlaylistReadiness // 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( func newPlayerPlaylist(
@@ -40,7 +44,7 @@ func newPlayerPlaylist(
} }
commands := make(chan playback.PlaylistCommand, 32) commands := make(chan playback.PlaylistCommand, 32)
readiness := make(chan playback.PlaylistReadiness, 8) events := make(chan playback.PlaylistEvent, 8)
controller, err := playback.NewPlaylistController( controller, err := playback.NewPlaylistController(
playlist, playlist,
retry, retry,
@@ -49,12 +53,12 @@ func newPlayerPlaylist(
if err != nil { if err != nil {
return nil, err return nil, err
} }
coordinator, err := playback.NewPlaylistReadinessCoordinator( coordinator, err := playback.NewPlaylistEventCoordinator(
controller, controller,
player.Controller, player.Controller,
player.Status, player.Status,
readiness, events,
playlistReadinessInterval, playlistEventInterval,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -62,9 +66,9 @@ func newPlayerPlaylist(
return &playerPlaylist{ return &playerPlaylist{
Controller: controller, Controller: controller,
Coordinator: coordinator, coordinator: coordinator,
Commands: commands, commands: commands,
Readiness: readiness, events: events,
}, nil }, nil
} }
@@ -74,10 +78,10 @@ func (p *playerPlaylist) Run(ctx context.Context) error {
results := make(chan error, 2) results := make(chan error, 2)
go func() { go func() {
results <- p.Controller.Run(runCtx, p.Commands, p.Readiness) results <- p.Controller.Run(runCtx, p.commands, p.events)
}() }()
go func() { go func() {
results <- p.Coordinator.Run(runCtx) results <- p.coordinator.Run(runCtx)
}() }()
first := <-results first := <-results
@@ -121,7 +125,7 @@ func (p *playerPlaylist) Resume() bool {
func (p *playerPlaylist) enqueue(command playback.PlaylistCommand) bool { func (p *playerPlaylist) enqueue(command playback.PlaylistCommand) bool {
select { select {
case p.Commands <- command: case p.commands <- command:
return true return true
default: default:
return false return false
+4 -4
View File
@@ -104,10 +104,10 @@ func TestNewPlayerPlaylistWiresComponents(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("newPlayerPlaylist() error = %v", err) 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) 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) t.Fatalf("runtime channels = %#v", runtime)
} }
} }
@@ -135,7 +135,7 @@ func TestPlayerPlaylistNavigationHelpers(t *testing.T) {
if !test.send() { if !test.send() {
t.Fatal("navigation helper returned false") 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) t.Fatalf("navigation command = %#v, want %#v", got, test.want)
} }
} }
@@ -150,7 +150,7 @@ func TestPlayerPlaylistNavigationQueueFull(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("newPlayerPlaylist() error = %v", err) t.Fatalf("newPlayerPlaylist() error = %v", err)
} }
for range cap(runtime.Commands) { for range cap(runtime.commands) {
if !runtime.Next() { if !runtime.Next() {
t.Fatal("queue filled before reaching capacity") t.Fatal("queue filled before reaching capacity")
} }
+2 -2
View File
@@ -4,8 +4,8 @@ Size=400,400
Collapsed=0 Collapsed=0
[Window][Settings & Info] [Window][Settings & Info]
Pos=1220,0 Pos=580,0
Size=700,1080 Size=700,720
Collapsed=0 Collapsed=0
[Window][Stats] [Window][Stats]
+11 -12
View File
@@ -20,10 +20,6 @@ type PlaylistEvent struct {
Failure Status 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 { type playlistTimer interface {
C() <-chan time.Time C() <-chan time.Time
Stop() bool Stop() bool
@@ -100,7 +96,7 @@ func NewPlaylistController(
func (c *PlaylistController) Run( func (c *PlaylistController) Run(
ctx context.Context, ctx context.Context,
commands <-chan PlaylistCommand, commands <-chan PlaylistCommand,
readiness <-chan PlaylistReadiness, events <-chan PlaylistEvent,
) error { ) error {
state := PlaylistState{} state := PlaylistState{}
revision := uint64(0) revision := uint64(0)
@@ -182,13 +178,13 @@ func (c *PlaylistController) Run(
state = next state = next
c.publish(state, revision, timing) c.publish(state, revision, timing)
case ready, ok := <-readiness: case event, ok := <-events:
if !ok { if !ok {
readiness = nil events = nil
continue continue
} }
if ready.Kind == PlaylistEventFailed { if event.Kind == PlaylistEventFailed {
if ready.Revision != revision { if event.Revision != revision {
continue continue
} }
stopTimer() stopTimer()
@@ -197,7 +193,7 @@ func (c *PlaylistController) Run(
EntryName: c.playlist.Entries[state.CurrentIndex].Name, EntryName: c.playlist.Entries[state.CurrentIndex].Name,
Revision: revision, Revision: revision,
Policy: c.playlist.OnFailure, Policy: c.playlist.OnFailure,
Status: ready.Failure, Status: event.Failure,
}) })
// A failed entry must not retain a live or apparently active // A failed entry must not retain a live or apparently active
// duration clock, even when the policy is to wait. // duration clock, even when the policy is to wait.
@@ -228,8 +224,11 @@ func (c *PlaylistController) Run(
c.publish(state, revision, timing) c.publish(state, revision, timing)
continue continue
} }
if event.Kind != PlaylistEventReady {
continue
}
if timing.Paused && if timing.Paused &&
ready.Revision == timing.Revision && event.Revision == timing.Revision &&
timing.Duration > 0 && timing.Duration > 0 &&
!timing.Expired { !timing.Expired {
timing.Ready = true timing.Ready = true
@@ -238,7 +237,7 @@ func (c *PlaylistController) Run(
} }
nextTiming, started := StartPlaylistTiming( nextTiming, started := StartPlaylistTiming(
timing, timing,
ready.Revision, event.Revision,
c.now(), c.now(),
) )
if !started { if !started {
@@ -68,9 +68,11 @@ func TestPlaylistControllerStartsOneTimerForMatchingReadiness(t *testing.T) {
return snapshot.Revision == 1 return snapshot.Revision == 1
}) })
readiness <- PlaylistReadiness{Revision: 0} readiness <- PlaylistEvent{Revision: 0}
assertNoPlaylistTimer(t, timers) 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) timer := receiveFakePlaylistTimer(t, timers)
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool { 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) { if snapshot.Timing.Deadline != now.Add(10*time.Second) {
t.Fatalf("deadline = %v, want %v", 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) assertNoPlaylistTimer(t, timers)
if timer.isStopped() { if timer.isStopped() {
t.Fatal("timer stopped after duplicate readiness") t.Fatal("timer stopped after duplicate readiness")
@@ -101,7 +103,7 @@ func TestPlaylistControllerTimerExpiryAdvances(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistNext} commands <- PlaylistCommand{Kind: PlaylistNext}
_ = receivePlaylistSession(t, sessions) _ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1} readiness <- PlaylistEvent{Revision: 1}
timer := receiveFakePlaylistTimer(t, timers) timer := receiveFakePlaylistTimer(t, timers)
timer.fire(now.Add(10 * time.Second)) timer.fire(now.Add(10 * time.Second))
@@ -132,7 +134,7 @@ func TestPlaylistControllerLoopingTimerExpiryWraps(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1} commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
_ = receivePlaylistSession(t, sessions) _ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1} readiness <- PlaylistEvent{Revision: 1}
timer := receiveFakePlaylistTimer(t, timers) timer := receiveFakePlaylistTimer(t, timers)
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool { waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Timing.Started return snapshot.Timing.Started
@@ -160,7 +162,7 @@ func TestPlaylistControllerFinalExpiryStopsWithoutCommand(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1} commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
_ = receivePlaylistSession(t, sessions) _ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1} readiness <- PlaylistEvent{Revision: 1}
timer := receiveFakePlaylistTimer(t, timers) timer := receiveFakePlaylistTimer(t, timers)
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool { waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Timing.Started return snapshot.Timing.Started
@@ -192,7 +194,7 @@ func TestPlaylistControllerManualSelectionStopsOldTimer(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistNext} commands <- PlaylistCommand{Kind: PlaylistNext}
_ = receivePlaylistSession(t, sessions) _ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1} readiness <- PlaylistEvent{Revision: 1}
oldTimer := receiveFakePlaylistTimer(t, timers) oldTimer := receiveFakePlaylistTimer(t, timers)
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1} commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
@@ -229,7 +231,7 @@ func TestPlaylistControllerZeroDurationDoesNotCreateTimer(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistNext} commands <- PlaylistCommand{Kind: PlaylistNext}
_ = receivePlaylistSession(t, sessions) _ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1} readiness <- PlaylistEvent{Revision: 1}
assertNoPlaylistTimer(t, timers) assertNoPlaylistTimer(t, timers)
close(commands) close(commands)
@@ -244,7 +246,7 @@ func TestPlaylistControllerCancellationStopsTimer(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistNext} commands <- PlaylistCommand{Kind: PlaylistNext}
_ = receivePlaylistSession(t, sessions) _ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1} readiness <- PlaylistEvent{Revision: 1}
timer := receiveFakePlaylistTimer(t, timers) timer := receiveFakePlaylistTimer(t, timers)
cancel() cancel()
if err := waitForPlaylistResult(t, result); !errors.Is(err, context.Canceled) { if err := waitForPlaylistResult(t, result); !errors.Is(err, context.Canceled) {
@@ -262,7 +264,7 @@ func TestPlaylistControllerPauseAndResumeTimer(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistNext} commands <- PlaylistCommand{Kind: PlaylistNext}
_ = receivePlaylistSession(t, sessions) _ = receivePlaylistSession(t, sessions)
readiness <- PlaylistReadiness{Revision: 1} readiness <- PlaylistEvent{Revision: 1}
oldTimer := receiveFakePlaylistTimer(t, timers) oldTimer := receiveFakePlaylistTimer(t, timers)
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool { waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Timing.Started return snapshot.Timing.Started
@@ -344,7 +346,7 @@ func TestPlaylistControllerRecordsQueuedReadinessWhilePaused(t *testing.T) {
return snapshot.Timing.Paused return snapshot.Timing.Paused
}) })
readiness <- PlaylistReadiness{Revision: 1} readiness <- PlaylistEvent{Revision: 1}
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool { waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Timing.Paused && snapshot.Timing.Ready return snapshot.Timing.Paused && snapshot.Timing.Ready
}) })
@@ -368,7 +370,7 @@ func startTimedPlaylistController(
) ( ) (
*PlaylistController, *PlaylistController,
chan PlaylistCommand, chan PlaylistCommand,
chan PlaylistReadiness, chan PlaylistEvent,
chan SessionCommand, chan SessionCommand,
chan *fakePlaylistTimer, chan *fakePlaylistTimer,
time.Time, time.Time,
@@ -390,7 +392,7 @@ func startTimedPlaylistController(
return timer return timer
} }
commands := make(chan PlaylistCommand, 16) commands := make(chan PlaylistCommand, 16)
readiness := make(chan PlaylistReadiness, 16) readiness := make(chan PlaylistEvent, 16)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1) result := make(chan error, 1)
go func() { result <- controller.Run(ctx, commands, readiness) }() go func() { result <- controller.Run(ctx, commands, readiness) }()
@@ -18,45 +18,45 @@ type PlaybackStatusSnapshotSource interface {
SnapshotAll() PlaybackStatusSnapshot SnapshotAll() PlaybackStatusSnapshot
} }
type playlistReadinessTicker interface { type playlistEventTicker interface {
C() <-chan time.Time C() <-chan time.Time
Stop() Stop()
} }
type playlistReadinessTickerFactory func(time.Duration) playlistReadinessTicker type playlistEventTickerFactory func(time.Duration) playlistEventTicker
type realPlaylistReadinessTicker struct { type realPlaylistEventTicker struct {
ticker *time.Ticker ticker *time.Ticker
} }
func (t realPlaylistReadinessTicker) C() <-chan time.Time { return t.ticker.C } func (t realPlaylistEventTicker) C() <-chan time.Time { return t.ticker.C }
func (t realPlaylistReadinessTicker) Stop() { t.ticker.Stop() } func (t realPlaylistEventTicker) Stop() { t.ticker.Stop() }
var ( var (
ErrPlaylistSnapshotSourceRequired = errors.New("playlist snapshot source is required") ErrPlaylistSnapshotSourceRequired = errors.New("playlist snapshot source is required")
ErrSessionSnapshotSourceRequired = errors.New("session snapshot source is required") ErrSessionSnapshotSourceRequired = errors.New("session snapshot source is required")
ErrStatusSnapshotSourceRequired = errors.New("playback status snapshot source is required") ErrStatusSnapshotSourceRequired = errors.New("playback status snapshot source is required")
ErrPlaylistReadinessOutputRequired = errors.New("playlist readiness output channel is required") ErrPlaylistEventOutputRequired = errors.New("playlist event output channel is required")
ErrPlaylistReadinessInterval = errors.New("playlist readiness interval must be positive") ErrPlaylistEventInterval = errors.New("playlist event interval must be positive")
) )
type PlaylistReadinessCoordinator struct { type PlaylistEventCoordinator struct {
playlist PlaylistSnapshotSource playlist PlaylistSnapshotSource
session SessionSnapshotSource session SessionSnapshotSource
statuses PlaybackStatusSnapshotSource statuses PlaybackStatusSnapshotSource
output chan<- PlaylistReadiness output chan<- PlaylistEvent
interval time.Duration interval time.Duration
newTicker playlistReadinessTickerFactory newTicker playlistEventTickerFactory
} }
func NewPlaylistReadinessCoordinator( func NewPlaylistEventCoordinator(
playlist PlaylistSnapshotSource, playlist PlaylistSnapshotSource,
session SessionSnapshotSource, session SessionSnapshotSource,
statuses PlaybackStatusSnapshotSource, statuses PlaybackStatusSnapshotSource,
output chan<- PlaylistReadiness, output chan<- PlaylistEvent,
interval time.Duration, interval time.Duration,
) (*PlaylistReadinessCoordinator, error) { ) (*PlaylistEventCoordinator, error) {
if playlist == nil { if playlist == nil {
return nil, ErrPlaylistSnapshotSourceRequired return nil, ErrPlaylistSnapshotSourceRequired
} }
@@ -67,25 +67,25 @@ func NewPlaylistReadinessCoordinator(
return nil, ErrStatusSnapshotSourceRequired return nil, ErrStatusSnapshotSourceRequired
} }
if output == nil { if output == nil {
return nil, ErrPlaylistReadinessOutputRequired return nil, ErrPlaylistEventOutputRequired
} }
if interval <= 0 { if interval <= 0 {
return nil, ErrPlaylistReadinessInterval return nil, ErrPlaylistEventInterval
} }
return &PlaylistReadinessCoordinator{ return &PlaylistEventCoordinator{
playlist: playlist, playlist: playlist,
session: session, session: session,
statuses: statuses, statuses: statuses,
output: output, output: output,
interval: interval, interval: interval,
newTicker: func(interval time.Duration) playlistReadinessTicker { newTicker: func(interval time.Duration) playlistEventTicker {
return realPlaylistReadinessTicker{ticker: time.NewTicker(interval)} return realPlaylistEventTicker{ticker: time.NewTicker(interval)}
}, },
}, nil }, nil
} }
func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error { func (c *PlaylistEventCoordinator) Run(ctx context.Context) error {
ticker := c.newTicker(c.interval) ticker := c.newTicker(c.interval)
defer ticker.Stop() defer ticker.Stop()
@@ -139,7 +139,7 @@ func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
continue continue
} }
ready := PlaylistReadiness{Revision: playlistSnapshot.Revision} ready := PlaylistEvent{Revision: playlistSnapshot.Revision}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
@@ -63,55 +63,55 @@ func (s *fakePlaybackStatusSnapshotSource) set(snapshot PlaybackStatusSnapshot)
s.mu.Unlock() s.mu.Unlock()
} }
type fakePlaylistReadinessTicker struct { type fakePlaylistEventTicker struct {
ch chan time.Time ch chan time.Time
mu sync.Mutex mu sync.Mutex
stopped bool stopped bool
} }
func newFakePlaylistReadinessTicker() *fakePlaylistReadinessTicker { func newFakePlaylistEventTicker() *fakePlaylistEventTicker {
return &fakePlaylistReadinessTicker{ch: make(chan time.Time, 16)} return &fakePlaylistEventTicker{ch: make(chan time.Time, 16)}
} }
func (t *fakePlaylistReadinessTicker) C() <-chan time.Time { return t.ch } func (t *fakePlaylistEventTicker) C() <-chan time.Time { return t.ch }
func (t *fakePlaylistReadinessTicker) Stop() { func (t *fakePlaylistEventTicker) Stop() {
t.mu.Lock() t.mu.Lock()
t.stopped = true t.stopped = true
t.mu.Unlock() t.mu.Unlock()
} }
func (t *fakePlaylistReadinessTicker) tick() { t.ch <- time.Now() } func (t *fakePlaylistEventTicker) tick() { t.ch <- time.Now() }
func (t *fakePlaylistReadinessTicker) isStopped() bool { func (t *fakePlaylistEventTicker) isStopped() bool {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
return t.stopped return t.stopped
} }
func TestNewPlaylistReadinessCoordinatorValidatesDependencies(t *testing.T) { func TestNewPlaylistEventCoordinatorValidatesDependencies(t *testing.T) {
playlist := &fakePlaylistSnapshotSource{} playlist := &fakePlaylistSnapshotSource{}
session := &fakeSessionSnapshotSource{} session := &fakeSessionSnapshotSource{}
statuses := &fakePlaybackStatusSnapshotSource{} statuses := &fakePlaybackStatusSnapshotSource{}
output := make(chan PlaylistReadiness) output := make(chan PlaylistEvent)
tests := []struct { tests := []struct {
name string name string
playlist PlaylistSnapshotSource playlist PlaylistSnapshotSource
session SessionSnapshotSource session SessionSnapshotSource
statuses PlaybackStatusSnapshotSource statuses PlaybackStatusSnapshotSource
output chan<- PlaylistReadiness output chan<- PlaylistEvent
interval time.Duration interval time.Duration
wantErr error wantErr error
}{ }{
{name: "playlist", session: session, statuses: statuses, output: output, interval: time.Millisecond, wantErr: ErrPlaylistSnapshotSourceRequired}, {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: "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: "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: "output", playlist: playlist, session: session, statuses: statuses, interval: time.Millisecond, wantErr: ErrPlaylistEventOutputRequired},
{name: "interval", playlist: playlist, session: session, statuses: statuses, output: output, wantErr: ErrPlaylistReadinessInterval}, {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}, {name: "valid", playlist: playlist, session: session, statuses: statuses, output: output, interval: time.Millisecond},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
coordinator, err := NewPlaylistReadinessCoordinator( coordinator, err := NewPlaylistEventCoordinator(
test.playlist, test.playlist,
test.session, test.session,
test.statuses, test.statuses,
@@ -119,7 +119,7 @@ func TestNewPlaylistReadinessCoordinatorValidatesDependencies(t *testing.T) {
test.interval, test.interval,
) )
if !errors.Is(err, test.wantErr) { 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 { if test.wantErr != nil && coordinator != nil {
t.Fatalf("coordinator = %#v, want nil", coordinator) 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) playlist, session, statuses := readyVideoSnapshots(1)
output := make(chan PlaylistReadiness, 4) output := make(chan PlaylistEvent, 4)
coordinator, ticker, cancel, result := startReadinessCoordinator( coordinator, ticker, cancel, result := startEventCoordinator(
t, t,
playlist, playlist,
session, session,
@@ -188,16 +188,16 @@ func TestPlaylistReadinessCoordinatorEmitsOncePerRevision(t *testing.T) {
defer cancel() defer cancel()
ticker.tick() 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) t.Fatalf("readiness revision = %d, want 1", got.Revision)
} }
ticker.tick() ticker.tick()
assertNoPlaylistReadiness(t, output) assertNoPlaylistEvent(t, output)
next := playlistSnapshotForVideo(2) next := playlistSnapshotForVideo(2)
playlist.set(next, true) playlist.set(next, true)
ticker.tick() 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) 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) playlist, session, statuses := readyVideoSnapshots(1)
output := make(chan PlaylistReadiness, 1) output := make(chan PlaylistEvent, 1)
_, ticker, cancel, result := startReadinessCoordinator(t, playlist, session, statuses, output) _, ticker, cancel, result := startEventCoordinator(t, playlist, session, statuses, output)
defer cancel() defer cancel()
tests := []struct { tests := []struct {
@@ -256,7 +256,7 @@ func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
statuses.set(validStatuses.snapshot) statuses.set(validStatuses.snapshot)
test.mutate() test.mutate()
ticker.tick() ticker.tick()
assertNoPlaylistReadiness(t, output) assertNoPlaylistEvent(t, output)
}) })
} }
@@ -264,10 +264,10 @@ func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
_ = waitForPlaylistResult(t, result) _ = waitForPlaylistResult(t, result)
} }
func TestPlaylistReadinessCoordinatorCancellationWhileBlockedSending(t *testing.T) { func TestPlaylistEventCoordinatorCancellationWhileBlockedSending(t *testing.T) {
playlist, session, statuses := readyVideoSnapshots(1) playlist, session, statuses := readyVideoSnapshots(1)
output := make(chan PlaylistReadiness) output := make(chan PlaylistEvent)
_, ticker, cancel, result := startReadinessCoordinator(t, playlist, session, statuses, output) _, ticker, cancel, result := startEventCoordinator(t, playlist, session, statuses, output)
ticker.tick() ticker.tick()
time.Sleep(time.Millisecond) time.Sleep(time.Millisecond)
@@ -322,15 +322,15 @@ func playlistSnapshotForVideo(revision uint64) PlaylistSnapshot {
} }
} }
func startReadinessCoordinator( func startEventCoordinator(
t *testing.T, t *testing.T,
playlist PlaylistSnapshotSource, playlist PlaylistSnapshotSource,
session SessionSnapshotSource, session SessionSnapshotSource,
statuses PlaybackStatusSnapshotSource, statuses PlaybackStatusSnapshotSource,
output chan<- PlaylistReadiness, output chan<- PlaylistEvent,
) (*PlaylistReadinessCoordinator, *fakePlaylistReadinessTicker, context.CancelFunc, <-chan error) { ) (*PlaylistEventCoordinator, *fakePlaylistEventTicker, context.CancelFunc, <-chan error) {
t.Helper() t.Helper()
coordinator, err := NewPlaylistReadinessCoordinator( coordinator, err := NewPlaylistEventCoordinator(
playlist, playlist,
session, session,
statuses, statuses,
@@ -338,28 +338,28 @@ func startReadinessCoordinator(
time.Millisecond, time.Millisecond,
) )
if err != nil { if err != nil {
t.Fatalf("NewPlaylistReadinessCoordinator() error = %v", err) t.Fatalf("NewPlaylistEventCoordinator() error = %v", err)
} }
ticker := newFakePlaylistReadinessTicker() ticker := newFakePlaylistEventTicker()
coordinator.newTicker = func(time.Duration) playlistReadinessTicker { return ticker } coordinator.newTicker = func(time.Duration) playlistEventTicker { return ticker }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1) result := make(chan error, 1)
go func() { result <- coordinator.Run(ctx) }() go func() { result <- coordinator.Run(ctx) }()
return coordinator, ticker, cancel, result 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() t.Helper()
select { select {
case readiness := <-output: case readiness := <-output:
return readiness return readiness
case <-time.After(time.Second): case <-time.After(time.Second):
t.Fatal("timed out waiting for playlist readiness") 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() t.Helper()
select { select {
case readiness := <-output: case readiness := <-output:
+1 -1
View File
@@ -146,7 +146,7 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
t.Fatalf("NewPlaylistController() error = %v", err) t.Fatalf("NewPlaylistController() error = %v", err)
} }
commands := make(chan PlaylistCommand, 2) commands := make(chan PlaylistCommand, 2)
events := make(chan PlaylistReadiness, 2) events := make(chan PlaylistEvent, 2)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1) result := make(chan error, 1)
go func() { result <- controller.Run(ctx, commands, events) }() go func() { result <- controller.Run(ctx, commands, events) }()
+5 -5
View File
@@ -116,7 +116,7 @@ type playlistPipelineHarness struct {
sessions chan SessionCommand sessions chan SessionCommand
session *fakeSessionSnapshotSource session *fakeSessionSnapshotSource
statuses *fakePlaybackStatusSnapshotSource statuses *fakePlaybackStatusSnapshotSource
ticker *fakePlaylistReadinessTicker ticker *fakePlaylistEventTicker
cancel context.CancelFunc cancel context.CancelFunc
results chan error results chan error
} }
@@ -128,17 +128,17 @@ func startPlaylistPipeline(t *testing.T, playlist Playlist) *playlistPipelineHar
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
events := make(chan PlaylistReadiness, 8) events := make(chan PlaylistEvent, 8)
session := &fakeSessionSnapshotSource{} session := &fakeSessionSnapshotSource{}
statuses := &fakePlaybackStatusSnapshotSource{} statuses := &fakePlaybackStatusSnapshotSource{}
coordinator, err := NewPlaylistReadinessCoordinator( coordinator, err := NewPlaylistEventCoordinator(
controller, session, statuses, events, time.Millisecond, controller, session, statuses, events, time.Millisecond,
) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
ticker := newFakePlaylistReadinessTicker() ticker := newFakePlaylistEventTicker()
coordinator.newTicker = func(time.Duration) playlistReadinessTicker { return ticker } coordinator.newTicker = func(time.Duration) playlistEventTicker { return ticker }
commands := make(chan PlaylistCommand, 8) commands := make(chan PlaylistCommand, 8)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
results := make(chan error, 2) results := make(chan error, 2)