package playback import ( "context" "errors" "testing" ) type fakeAudioFactory struct { reader AudioReader err error calls int } func (f *fakeAudioFactory) OpenAudio( context.Context, FeedConfig, ) (AudioReader, error) { f.calls++ return f.reader, f.err } type fakeAudioReader struct { frames []AudioFrame readErr error closeErr error readCalls int closed bool read func(context.Context) (AudioFrame, error) } func (r *fakeAudioReader) ReadAudio(ctx context.Context) (AudioFrame, error) { r.readCalls++ if r.read != nil { return r.read(ctx) } if len(r.frames) == 0 { return AudioFrame{}, r.readErr } frame := r.frames[0] r.frames = r.frames[1:] return frame, nil } func (r *fakeAudioReader) Close() error { r.closed = true return r.closeErr } type fakeAudioSink struct { frames []AudioFrame err error } func (s *fakeAudioSink) ConsumeAudio(_ context.Context, frame AudioFrame) error { s.frames = append(s.frames, frame) return s.err } func TestRunAudioAttemptOpenFailure(t *testing.T) { openErr := errors.New("open failed") factory := &fakeAudioFactory{err: openErr} sink := &fakeAudioSink{} err := runAudioAttempt(context.Background(), factory, sink, FeedConfig{}) if !errors.Is(err, openErr) { t.Fatalf("runAudioAttempt() error = %v, want %v", err, openErr) } if factory.calls != 1 { t.Errorf("factory calls = %d, want 1", factory.calls) } if len(sink.frames) != 0 { t.Fatalf("consumed frame count = %d, want 0", len(sink.frames)) } } func TestRunAudioAttemptConsumesFrameWithoutCopyThenReturnsReadFailure(t *testing.T) { readErr := errors.New("read failed") wantFrame := AudioFrame{ Index: 42, SampleCount: 2, Channels: 2, SampleRateNumerator: 48000, SampleRateDenominator: 1, Samples: [][]byte{ {1, 2, 3, 4}, {5, 6, 7, 8}, }, } reader := &fakeAudioReader{ frames: []AudioFrame{wantFrame}, readErr: readErr, } sink := &fakeAudioSink{} err := runAudioAttempt( context.Background(), &fakeAudioFactory{reader: reader}, sink, FeedConfig{}, ) if !errors.Is(err, readErr) { t.Fatalf("runAudioAttempt() error = %v, want %v", err, readErr) } if !reader.closed { t.Fatal("reader was not closed") } if reader.readCalls != 2 { t.Errorf("read calls = %d, want 2", reader.readCalls) } if len(sink.frames) != 1 { t.Fatalf("consumed frame count = %d, want 1", len(sink.frames)) } got := sink.frames[0] if got.Index != wantFrame.Index || got.SampleCount != wantFrame.SampleCount || got.Channels != wantFrame.Channels || got.SampleRateNumerator != wantFrame.SampleRateNumerator || got.SampleRateDenominator != wantFrame.SampleRateDenominator { t.Errorf("consumed frame metadata = %+v, want %+v", got, wantFrame) } for channel := range wantFrame.Samples { if &got.Samples[channel][0] != &wantFrame.Samples[channel][0] { t.Fatalf("channel %d samples were copied", channel) } } } func TestRunAudioAttemptSinkFailureStopsReadingAndCloses(t *testing.T) { sinkErr := errors.New("audio output unavailable") reader := &fakeAudioReader{ frames: []AudioFrame{ {Index: 1, Samples: [][]byte{{1}}}, {Index: 2, Samples: [][]byte{{2}}}, }, } err := runAudioAttempt( context.Background(), &fakeAudioFactory{reader: reader}, &fakeAudioSink{err: sinkErr}, FeedConfig{}, ) if !errors.Is(err, sinkErr) { t.Fatalf("runAudioAttempt() error = %v, want %v", err, sinkErr) } var typedErr *audioSinkError if !errors.As(err, &typedErr) { t.Fatalf("runAudioAttempt() error type = %T, want *audioSinkError", err) } if reader.readCalls != 1 { t.Errorf("read calls = %d, want 1", reader.readCalls) } if !reader.closed { t.Fatal("reader was not closed") } } func TestRunAudioAttemptCanceledRead(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) reader := &fakeAudioReader{ read: func(ctx context.Context) (AudioFrame, error) { cancel() return AudioFrame{}, ctx.Err() }, } err := runAudioAttempt( ctx, &fakeAudioFactory{reader: reader}, &fakeAudioSink{}, FeedConfig{}, ) if !errors.Is(err, context.Canceled) { t.Fatalf("runAudioAttempt() error = %v, want context.Canceled", err) } if !reader.closed { t.Fatal("reader was not closed") } } func TestRunAudioAttemptJoinsReadAndCloseErrors(t *testing.T) { readErr := errors.New("read failed") closeErr := errors.New("close failed") reader := &fakeAudioReader{readErr: readErr, closeErr: closeErr} err := runAudioAttempt( context.Background(), &fakeAudioFactory{reader: reader}, &fakeAudioSink{}, FeedConfig{}, ) if !errors.Is(err, readErr) { t.Errorf("runAudioAttempt() error does not contain read error: %v", err) } if !errors.Is(err, closeErr) { t.Errorf("runAudioAttempt() error does not contain close error: %v", err) } }