add audio playback attempt
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user