package playback import ( "context" "errors" "testing" ) type fakeSyncFactory struct { reader SyncReader err error calls int videoConfig FeedConfig audioConfig FeedConfig } func (f *fakeSyncFactory) OpenSync( _ context.Context, videoConfig FeedConfig, audioConfig FeedConfig, ) (SyncReader, error) { f.calls++ f.videoConfig = videoConfig f.audioConfig = audioConfig return f.reader, f.err } type fakeSyncReader struct { frames []SyncFrame readErr error closeErr error readCalls int closed bool read func(context.Context) (SyncFrame, error) } func (r *fakeSyncReader) ReadSync(ctx context.Context) (SyncFrame, error) { r.readCalls++ if r.read != nil { return r.read(ctx) } if len(r.frames) == 0 { return SyncFrame{}, r.readErr } frame := r.frames[0] r.frames = r.frames[1:] return frame, nil } func (r *fakeSyncReader) Close() error { r.closed = true return r.closeErr } type orderedVideoSink struct { order *[]string err error frame VideoFrame } func (s *orderedVideoSink) ConsumeVideo(_ context.Context, frame VideoFrame) error { *s.order = append(*s.order, "video") s.frame = frame return s.err } type orderedAudioSink struct { order *[]string err error frame AudioFrame } func (s *orderedAudioSink) ConsumeAudio(_ context.Context, frame AudioFrame) error { *s.order = append(*s.order, "audio") s.frame = frame return s.err } func TestRunSyncAttemptOpenFailure(t *testing.T) { openErr := errors.New("open failed") factory := &fakeSyncFactory{err: openErr} videoConfig := FeedConfig{Domain: "/mxl", UUID: "video", Active: true} audioConfig := FeedConfig{Domain: "/mxl", UUID: "audio", Active: true} err := runSyncAttempt( context.Background(), factory, &fakeVideoSink{}, &fakeAudioSink{}, videoConfig, audioConfig, ) if !errors.Is(err, openErr) { t.Fatalf("runSyncAttempt() error = %v, want %v", err, openErr) } if factory.calls != 1 || factory.videoConfig != videoConfig || factory.audioConfig != audioConfig { t.Fatalf( "factory call = %d, video %#v, audio %#v", factory.calls, factory.videoConfig, factory.audioConfig, ) } } func TestRunSyncAttemptConsumesBorrowedPairInOrderThenReturnsReadError(t *testing.T) { readErr := errors.New("sync read failed") videoPayload := []byte{1, 2, 3, 4} audioSamples := [][]byte{{5, 6, 7, 8}} want := SyncFrame{ Video: VideoFrame{Index: 10, Payload: videoPayload}, Audio: AudioFrame{Index: 20, Samples: audioSamples}, } reader := &fakeSyncReader{frames: []SyncFrame{want}, readErr: readErr} var order []string videoSink := &orderedVideoSink{order: &order} audioSink := &orderedAudioSink{order: &order} err := runSyncAttempt( context.Background(), &fakeSyncFactory{reader: reader}, videoSink, audioSink, FeedConfig{}, FeedConfig{}, ) if !errors.Is(err, readErr) { t.Fatalf("runSyncAttempt() error = %v, want %v", err, readErr) } if len(order) != 2 || order[0] != "video" || order[1] != "audio" { t.Fatalf("sink order = %v, want [video audio]", order) } if &videoSink.frame.Payload[0] != &videoPayload[0] { t.Fatal("video payload was copied") } if &audioSink.frame.Samples[0][0] != &audioSamples[0][0] { t.Fatal("audio samples were copied") } if reader.readCalls != 2 || !reader.closed { t.Fatalf("reader calls = %d, closed = %t; want 2, true", reader.readCalls, reader.closed) } } func TestRunSyncAttemptVideoSinkFailureSkipsAudio(t *testing.T) { sinkErr := errors.New("video output failed") reader := &fakeSyncReader{frames: []SyncFrame{{}}} var order []string err := runSyncAttempt( context.Background(), &fakeSyncFactory{reader: reader}, &orderedVideoSink{order: &order, err: sinkErr}, &orderedAudioSink{order: &order}, FeedConfig{}, FeedConfig{}, ) if !errors.Is(err, sinkErr) { t.Fatalf("runSyncAttempt() error = %v, want %v", err, sinkErr) } var typedErr *videoSinkError if !errors.As(err, &typedErr) { t.Fatalf("runSyncAttempt() error type = %T, want *videoSinkError", err) } if len(order) != 1 || order[0] != "video" { t.Fatalf("sink order = %v, want [video]", order) } } func TestRunSyncAttemptAudioSinkFailureFollowsVideo(t *testing.T) { sinkErr := errors.New("audio output failed") reader := &fakeSyncReader{frames: []SyncFrame{{}}} var order []string err := runSyncAttempt( context.Background(), &fakeSyncFactory{reader: reader}, &orderedVideoSink{order: &order}, &orderedAudioSink{order: &order, err: sinkErr}, FeedConfig{}, FeedConfig{}, ) if !errors.Is(err, sinkErr) { t.Fatalf("runSyncAttempt() error = %v, want %v", err, sinkErr) } var typedErr *audioSinkError if !errors.As(err, &typedErr) { t.Fatalf("runSyncAttempt() error type = %T, want *audioSinkError", err) } if len(order) != 2 || order[0] != "video" || order[1] != "audio" { t.Fatalf("sink order = %v, want [video audio]", order) } } func TestRunSyncAttemptCancellationClosesReader(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) reader := &fakeSyncReader{ read: func(ctx context.Context) (SyncFrame, error) { cancel() return SyncFrame{}, ctx.Err() }, } err := runSyncAttempt( ctx, &fakeSyncFactory{reader: reader}, &fakeVideoSink{}, &fakeAudioSink{}, FeedConfig{}, FeedConfig{}, ) if !errors.Is(err, context.Canceled) { t.Fatalf("runSyncAttempt() error = %v, want context.Canceled", err) } if !reader.closed { t.Fatal("reader was not closed") } } func TestRunSyncAttemptJoinsReadAndCloseErrors(t *testing.T) { readErr := errors.New("read failed") closeErr := errors.New("close failed") reader := &fakeSyncReader{readErr: readErr, closeErr: closeErr} err := runSyncAttempt( context.Background(), &fakeSyncFactory{reader: reader}, &fakeVideoSink{}, &fakeAudioSink{}, FeedConfig{}, FeedConfig{}, ) if !errors.Is(err, readErr) || !errors.Is(err, closeErr) { t.Fatalf("runSyncAttempt() error = %v, want read and close errors", err) } }