Files
2026-08-27 23:49:43 +03:00

205 lines
4.7 KiB
Go

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)
}
}