304 lines
7.6 KiB
Go
304 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"mxl-player/internal/playback"
|
|
)
|
|
|
|
type playlistTestVideoSlot struct{}
|
|
|
|
func (playlistTestVideoSlot) Run(
|
|
ctx context.Context,
|
|
initial playback.FeedConfig,
|
|
commands <-chan playback.FeedConfig,
|
|
) error {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case _, ok := <-commands:
|
|
if !ok {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type playlistTestAudioSlot struct{}
|
|
|
|
func (playlistTestAudioSlot) Run(
|
|
ctx context.Context,
|
|
initial playback.FeedConfig,
|
|
commands <-chan playback.FeedConfig,
|
|
) error {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case _, ok := <-commands:
|
|
if !ok {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type playlistTestSyncSlot struct{}
|
|
|
|
func (playlistTestSyncSlot) Run(
|
|
ctx context.Context,
|
|
initial playback.SyncPairConfig,
|
|
commands <-chan playback.SyncPairConfig,
|
|
) error {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case _, ok := <-commands:
|
|
if !ok {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewPlayerPlaylistValidatesPlayer(t *testing.T) {
|
|
retry := playlistRuntimeRetry()
|
|
tests := []struct {
|
|
name string
|
|
player *playerPlayback
|
|
wantErr error
|
|
}{
|
|
{name: "nil player", wantErr: ErrPlayerPlaybackRequired},
|
|
{name: "nil controller", player: &playerPlayback{}, wantErr: ErrPlayerSessionControllerRequired},
|
|
{
|
|
name: "nil status store",
|
|
player: &playerPlayback{
|
|
Controller: newPlaylistTestSessionController(t),
|
|
},
|
|
wantErr: ErrPlayerStatusStoreRequired,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := newPlayerPlaylist(playback.Playlist{}, retry, test.player)
|
|
if !errors.Is(err, test.wantErr) {
|
|
t.Fatalf("newPlayerPlaylist() error = %v, want %v", err, test.wantErr)
|
|
}
|
|
if got != nil {
|
|
t.Fatalf("newPlayerPlaylist() = %#v, want nil", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewPlayerPlaylistWiresComponents(t *testing.T) {
|
|
player := newPlaylistTestPlayer(t)
|
|
runtime, err := newPlayerPlaylist(playback.Playlist{}, playlistRuntimeRetry(), player)
|
|
if err != nil {
|
|
t.Fatalf("newPlayerPlaylist() error = %v", err)
|
|
}
|
|
if runtime.Controller == nil || runtime.Coordinator == nil {
|
|
t.Fatalf("runtime components = %#v", runtime)
|
|
}
|
|
if runtime.Commands == nil || runtime.Readiness == nil {
|
|
t.Fatalf("runtime channels = %#v", runtime)
|
|
}
|
|
}
|
|
|
|
func TestPlayerPlaylistNavigationHelpers(t *testing.T) {
|
|
runtime, err := newPlayerPlaylist(
|
|
playback.Playlist{},
|
|
playlistRuntimeRetry(),
|
|
newPlaylistTestPlayer(t),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("newPlayerPlaylist() error = %v", err)
|
|
}
|
|
tests := []struct {
|
|
send func() bool
|
|
want playback.PlaylistCommand
|
|
}{
|
|
{send: func() bool { return runtime.Select(4) }, want: playback.PlaylistCommand{Kind: playback.PlaylistSelect, Index: 4}},
|
|
{send: runtime.Next, want: playback.PlaylistCommand{Kind: playback.PlaylistNext}},
|
|
{send: runtime.Previous, want: playback.PlaylistCommand{Kind: playback.PlaylistPrevious}},
|
|
}
|
|
for _, test := range tests {
|
|
if !test.send() {
|
|
t.Fatal("navigation helper returned false")
|
|
}
|
|
if got := <-runtime.Commands; got != test.want {
|
|
t.Fatalf("navigation command = %#v, want %#v", got, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlayerPlaylistNavigationQueueFull(t *testing.T) {
|
|
runtime, err := newPlayerPlaylist(
|
|
playback.Playlist{},
|
|
playlistRuntimeRetry(),
|
|
newPlaylistTestPlayer(t),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("newPlayerPlaylist() error = %v", err)
|
|
}
|
|
for range cap(runtime.Commands) {
|
|
if !runtime.Next() {
|
|
t.Fatal("queue filled before reaching capacity")
|
|
}
|
|
}
|
|
if runtime.Next() {
|
|
t.Fatal("Next() = true with full queue")
|
|
}
|
|
}
|
|
|
|
func TestPlayerPlaylistRunCancellationJoinsComponents(t *testing.T) {
|
|
runtime, err := newPlayerPlaylist(
|
|
playback.Playlist{},
|
|
playlistRuntimeRetry(),
|
|
newPlaylistTestPlayer(t),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("newPlayerPlaylist() error = %v", err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
result := make(chan error, 1)
|
|
go func() { result <- runtime.Run(ctx) }()
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-result:
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Run() error = %v, want %v", err, context.Canceled)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for playlist runtime cancellation")
|
|
}
|
|
}
|
|
|
|
func TestPlayerPlaylistTimedEntryAdvances(t *testing.T) {
|
|
retry := playlistRuntimeRetry()
|
|
player := newPlaylistTestPlayer(t)
|
|
playlist := playback.Playlist{
|
|
Entries: []playback.PlaylistEntry{
|
|
{
|
|
Video: playback.PlaylistFeed{Domain: "domain", UUID: "video-1"},
|
|
Duration: 15 * time.Millisecond,
|
|
},
|
|
{
|
|
Video: playback.PlaylistFeed{Domain: "domain", UUID: "video-2"},
|
|
},
|
|
},
|
|
}
|
|
runtime, err := newPlayerPlaylist(playlist, retry, player)
|
|
if err != nil {
|
|
t.Fatalf("newPlayerPlaylist() error = %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
sessionResult := make(chan error, 1)
|
|
go func() {
|
|
sessionResult <- player.Controller.Run(
|
|
ctx,
|
|
playback.SessionConfig{Retry: retry},
|
|
player.Commands,
|
|
)
|
|
}()
|
|
playlistResult := make(chan error, 1)
|
|
go func() { playlistResult <- runtime.Run(ctx) }()
|
|
|
|
if !runtime.Next() {
|
|
t.Fatal("Next() = false")
|
|
}
|
|
first := waitForPlayerSession(t, player.Controller, func(snapshot playback.SessionSnapshot) bool {
|
|
return snapshot.Desired.Video.UUID == "video-1"
|
|
})
|
|
player.Status.Observe(playback.Status{
|
|
Unit: playback.UnitVideo,
|
|
State: playback.StatePlaying,
|
|
Generation: first.Generation,
|
|
Feed: first.Plan.Video,
|
|
})
|
|
|
|
second := waitForPlayerSession(t, player.Controller, func(snapshot playback.SessionSnapshot) bool {
|
|
return snapshot.Desired.Video.UUID == "video-2"
|
|
})
|
|
if second.Desired.Audio.IsConfigured() {
|
|
t.Fatalf("advanced session audio = %#v, want unconfigured", second.Desired.Audio)
|
|
}
|
|
|
|
cancel()
|
|
if err := waitForPlayerRuntimeResult(t, playlistResult); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("playlist Run() error = %v, want %v", err, context.Canceled)
|
|
}
|
|
if err := waitForPlayerRuntimeResult(t, sessionResult); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("session Run() error = %v, want %v", err, context.Canceled)
|
|
}
|
|
}
|
|
|
|
func newPlaylistTestPlayer(t *testing.T) *playerPlayback {
|
|
t.Helper()
|
|
return &playerPlayback{
|
|
Controller: newPlaylistTestSessionController(t),
|
|
Commands: make(chan playback.SessionCommand, 32),
|
|
Status: playback.NewStatusStore(),
|
|
}
|
|
}
|
|
|
|
func newPlaylistTestSessionController(t *testing.T) *playback.SessionController {
|
|
t.Helper()
|
|
controller, err := playback.NewSessionController(
|
|
playlistTestVideoSlot{},
|
|
playlistTestAudioSlot{},
|
|
playlistTestSyncSlot{},
|
|
func(video, audio playback.FeedConfig) bool { return video.Domain == audio.Domain },
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewSessionController() error = %v", err)
|
|
}
|
|
return controller
|
|
}
|
|
|
|
func playlistRuntimeRetry() playback.RetryPolicy {
|
|
return playback.RetryPolicy{
|
|
MaxAttempts: 1,
|
|
InitialDelay: time.Millisecond,
|
|
MaxDelay: time.Millisecond,
|
|
}
|
|
}
|
|
|
|
func waitForPlayerSession(
|
|
t *testing.T,
|
|
controller *playback.SessionController,
|
|
predicate func(playback.SessionSnapshot) bool,
|
|
) playback.SessionSnapshot {
|
|
t.Helper()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if snapshot, ok := controller.Snapshot(); ok && predicate(snapshot) {
|
|
return snapshot
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
snapshot, _ := controller.Snapshot()
|
|
t.Fatalf("timed out waiting for session snapshot; latest = %#v", snapshot)
|
|
return playback.SessionSnapshot{}
|
|
}
|
|
|
|
func waitForPlayerRuntimeResult(t *testing.T, result <-chan error) error {
|
|
t.Helper()
|
|
select {
|
|
case err := <-result:
|
|
return err
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for runtime result")
|
|
return nil
|
|
}
|
|
}
|