369 lines
11 KiB
Go
369 lines
11 KiB
Go
package playback
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewPlaylistControllerValidatesConfiguration(t *testing.T) {
|
|
validPlaylist := navigationPlaylist(false)
|
|
validRetry := validPlaylistRetryPolicy()
|
|
validSessions := make(chan SessionCommand)
|
|
tests := []struct {
|
|
name string
|
|
playlist Playlist
|
|
retry RetryPolicy
|
|
sessions chan<- SessionCommand
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "invalid playlist",
|
|
playlist: Playlist{Entries: []PlaylistEntry{
|
|
{},
|
|
}},
|
|
retry: validRetry,
|
|
sessions: validSessions,
|
|
wantErr: ErrPlaylistEntryEmpty,
|
|
},
|
|
{
|
|
name: "invalid retry",
|
|
playlist: validPlaylist,
|
|
retry: RetryPolicy{},
|
|
sessions: validSessions,
|
|
wantErr: ErrInvalidRetryDelay,
|
|
},
|
|
{
|
|
name: "nil session commands",
|
|
playlist: validPlaylist,
|
|
retry: validRetry,
|
|
wantErr: ErrNilSessionCommandChannel,
|
|
},
|
|
{
|
|
name: "valid",
|
|
playlist: validPlaylist,
|
|
retry: validRetry,
|
|
sessions: validSessions,
|
|
},
|
|
{
|
|
name: "empty playlist is valid",
|
|
playlist: Playlist{},
|
|
retry: validRetry,
|
|
sessions: validSessions,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
controller, err := NewPlaylistController(test.playlist, test.retry, test.sessions)
|
|
if !errors.Is(err, test.wantErr) {
|
|
t.Fatalf("NewPlaylistController() error = %v, want %v", err, test.wantErr)
|
|
}
|
|
if test.wantErr != nil && controller != nil {
|
|
t.Fatalf("NewPlaylistController() controller = %#v, want nil", controller)
|
|
}
|
|
if test.wantErr == nil && controller == nil {
|
|
t.Fatal("NewPlaylistController() controller is nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlaylistControllerPublishesInitialSnapshot(t *testing.T) {
|
|
controller, commands, _, cancel, result := startPlaylistController(t, navigationPlaylist(false), 0)
|
|
defer cancel()
|
|
|
|
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
|
return !snapshot.State.HasSelection
|
|
})
|
|
if snapshot.Entry != (PlaylistEntry{}) {
|
|
t.Fatalf("initial entry = %#v, want zero value", snapshot.Entry)
|
|
}
|
|
|
|
close(commands)
|
|
if err := waitForPlaylistResult(t, result); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistControllerForwardsManualCommands(t *testing.T) {
|
|
controller, commands, sessions, cancel, result := startPlaylistController(t, navigationPlaylist(true), 4)
|
|
defer cancel()
|
|
|
|
tests := []struct {
|
|
command PlaylistCommand
|
|
wantIndex int
|
|
wantUUID string
|
|
}{
|
|
{command: PlaylistCommand{Kind: PlaylistSelect, Index: 1}, wantIndex: 1, wantUUID: "audio-2"},
|
|
{command: PlaylistCommand{Kind: PlaylistNext}, wantIndex: 2, wantUUID: "video-3"},
|
|
{command: PlaylistCommand{Kind: PlaylistPrevious}, wantIndex: 1, wantUUID: "audio-2"},
|
|
{command: PlaylistCommand{Kind: PlaylistSelect, Index: 1}, wantIndex: 1, wantUUID: "audio-2"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
commands <- test.command
|
|
session := receivePlaylistSession(t, sessions)
|
|
if session.Kind != CommandSetSession {
|
|
t.Fatalf("session kind = %v, want %v", session.Kind, CommandSetSession)
|
|
}
|
|
gotUUID := session.Session.Video.UUID
|
|
if gotUUID == "" {
|
|
gotUUID = session.Session.Audio.UUID
|
|
}
|
|
if gotUUID != test.wantUUID {
|
|
t.Fatalf("session UUID = %q, want %q", gotUUID, test.wantUUID)
|
|
}
|
|
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
|
return snapshot.State.HasSelection && snapshot.State.CurrentIndex == test.wantIndex
|
|
})
|
|
}
|
|
|
|
close(commands)
|
|
if err := waitForPlaylistResult(t, result); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistControllerNonLoopingBoundarySendsNothing(t *testing.T) {
|
|
_, commands, sessions, cancel, result := startPlaylistController(t, navigationPlaylist(false), 2)
|
|
defer cancel()
|
|
|
|
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 2}
|
|
_ = receivePlaylistSession(t, sessions)
|
|
commands <- PlaylistCommand{Kind: PlaylistNext}
|
|
|
|
select {
|
|
case command := <-sessions:
|
|
t.Fatalf("unexpected session command at boundary: %#v", command)
|
|
case <-time.After(20 * time.Millisecond):
|
|
}
|
|
|
|
close(commands)
|
|
if err := waitForPlaylistResult(t, result); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistControllerIgnoresInvalidCommand(t *testing.T) {
|
|
_, commands, sessions, cancel, result := startPlaylistController(t, navigationPlaylist(false), 2)
|
|
defer cancel()
|
|
|
|
commands <- PlaylistCommand{}
|
|
commands <- PlaylistCommand{Kind: PlaylistNext}
|
|
if session := receivePlaylistSession(t, sessions); session.Session.Video.UUID != "video-1" {
|
|
t.Fatalf("session after invalid command = %#v", session)
|
|
}
|
|
|
|
close(commands)
|
|
if err := waitForPlaylistResult(t, result); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistControllerCommitsStateAfterSessionDelivery(t *testing.T) {
|
|
sessions := make(chan SessionCommand)
|
|
controller, err := NewPlaylistController(navigationPlaylist(false), validPlaylistRetryPolicy(), sessions)
|
|
if err != nil {
|
|
t.Fatalf("NewPlaylistController() error = %v", err)
|
|
}
|
|
commands := make(chan PlaylistCommand, 1)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
result := make(chan error, 1)
|
|
go func() { result <- controller.Run(ctx, commands) }()
|
|
|
|
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
|
return !snapshot.State.HasSelection
|
|
})
|
|
commands <- PlaylistCommand{Kind: PlaylistNext}
|
|
time.Sleep(time.Millisecond)
|
|
snapshot, ok := controller.Snapshot()
|
|
if !ok || snapshot.State.HasSelection {
|
|
t.Fatalf("snapshot before delivery = %#v, %v; want no selection", snapshot, ok)
|
|
}
|
|
|
|
_ = receivePlaylistSession(t, sessions)
|
|
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
|
return snapshot.State.HasSelection && snapshot.State.CurrentIndex == 0
|
|
})
|
|
|
|
close(commands)
|
|
if err := waitForPlaylistResult(t, result); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistControllerClosedCommandsReturnsNil(t *testing.T) {
|
|
_, commands, _, cancel, result := startPlaylistController(t, navigationPlaylist(false), 0)
|
|
defer cancel()
|
|
close(commands)
|
|
|
|
if err := waitForPlaylistResult(t, result); err != nil {
|
|
t.Fatalf("Run() error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistControllerCancellationWhileReceiving(t *testing.T) {
|
|
_, _, _, cancel, result := startPlaylistController(t, navigationPlaylist(false), 0)
|
|
cancel()
|
|
|
|
if err := waitForPlaylistResult(t, result); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Run() error = %v, want %v", err, context.Canceled)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistControllerCancellationWhileSending(t *testing.T) {
|
|
sessions := make(chan SessionCommand)
|
|
controller, err := NewPlaylistController(navigationPlaylist(false), validPlaylistRetryPolicy(), sessions)
|
|
if err != nil {
|
|
t.Fatalf("NewPlaylistController() error = %v", err)
|
|
}
|
|
commands := make(chan PlaylistCommand, 1)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
result := make(chan error, 1)
|
|
go func() { result <- controller.Run(ctx, commands) }()
|
|
|
|
commands <- PlaylistCommand{Kind: PlaylistNext}
|
|
cancel()
|
|
if err := waitForPlaylistResult(t, result); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Run() error = %v, want %v", err, context.Canceled)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistControllerSnapshotConcurrentReads(t *testing.T) {
|
|
controller, commands, _, cancel, result := startPlaylistController(t, navigationPlaylist(true), 64)
|
|
defer cancel()
|
|
|
|
var readers sync.WaitGroup
|
|
for range 8 {
|
|
readers.Add(1)
|
|
go func() {
|
|
defer readers.Done()
|
|
for range 100 {
|
|
_, _ = controller.Snapshot()
|
|
}
|
|
}()
|
|
}
|
|
for range 32 {
|
|
commands <- PlaylistCommand{Kind: PlaylistNext}
|
|
}
|
|
readers.Wait()
|
|
close(commands)
|
|
if err := waitForPlaylistResult(t, result); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistControllerSnapshotRevision(t *testing.T) {
|
|
controller, commands, sessions, cancel, result := startPlaylistController(t, navigationPlaylist(false), 8)
|
|
defer cancel()
|
|
|
|
initial := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
|
return !snapshot.State.HasSelection
|
|
})
|
|
if initial.Revision != 0 {
|
|
t.Fatalf("initial revision = %d, want 0", initial.Revision)
|
|
}
|
|
|
|
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
|
|
_ = receivePlaylistSession(t, sessions)
|
|
selected := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
|
return snapshot.Revision == 1
|
|
})
|
|
if selected.State.CurrentIndex != 1 {
|
|
t.Fatalf("selected state = %#v, want index 1", selected.State)
|
|
}
|
|
|
|
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
|
|
_ = receivePlaylistSession(t, sessions)
|
|
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
|
return snapshot.Revision == 2
|
|
})
|
|
|
|
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 2}
|
|
_ = receivePlaylistSession(t, sessions)
|
|
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
|
return snapshot.Revision == 3
|
|
})
|
|
|
|
commands <- PlaylistCommand{Kind: PlaylistNext}
|
|
time.Sleep(time.Millisecond)
|
|
boundary, ok := controller.Snapshot()
|
|
if !ok || boundary.Revision != 3 {
|
|
t.Fatalf("boundary snapshot = %#v, %v; want revision 3", boundary, ok)
|
|
}
|
|
|
|
commands <- PlaylistCommand{}
|
|
time.Sleep(time.Millisecond)
|
|
invalid, ok := controller.Snapshot()
|
|
if !ok || invalid.Revision != 3 {
|
|
t.Fatalf("invalid-command snapshot = %#v, %v; want revision 3", invalid, ok)
|
|
}
|
|
|
|
close(commands)
|
|
if err := waitForPlaylistResult(t, result); err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func startPlaylistController(
|
|
t *testing.T,
|
|
playlist Playlist,
|
|
sessionBuffer int,
|
|
) (*PlaylistController, chan PlaylistCommand, chan SessionCommand, context.CancelFunc, <-chan error) {
|
|
t.Helper()
|
|
sessions := make(chan SessionCommand, sessionBuffer)
|
|
controller, err := NewPlaylistController(playlist, validPlaylistRetryPolicy(), sessions)
|
|
if err != nil {
|
|
t.Fatalf("NewPlaylistController() error = %v", err)
|
|
}
|
|
commands := make(chan PlaylistCommand, 64)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
result := make(chan error, 1)
|
|
go func() { result <- controller.Run(ctx, commands) }()
|
|
return controller, commands, sessions, cancel, result
|
|
}
|
|
|
|
func waitForPlaylistSnapshot(
|
|
t *testing.T,
|
|
controller *PlaylistController,
|
|
predicate func(PlaylistSnapshot) bool,
|
|
) PlaylistSnapshot {
|
|
t.Helper()
|
|
deadline := time.Now().Add(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 playlist snapshot; latest = %#v", snapshot)
|
|
return PlaylistSnapshot{}
|
|
}
|
|
|
|
func receivePlaylistSession(t *testing.T, sessions <-chan SessionCommand) SessionCommand {
|
|
t.Helper()
|
|
select {
|
|
case command := <-sessions:
|
|
return command
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for session command")
|
|
return SessionCommand{}
|
|
}
|
|
}
|
|
|
|
func waitForPlaylistResult(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 playlist controller")
|
|
return nil
|
|
}
|
|
}
|