Refactoring #3
@@ -0,0 +1,55 @@
|
||||
package playback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type videoSinkError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *videoSinkError) Error() string {
|
||||
return fmt.Sprintf("consume video: %v", e.err)
|
||||
}
|
||||
|
||||
func (e *videoSinkError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func runVideoAttempt(
|
||||
ctx context.Context,
|
||||
factory VideoReaderFactory,
|
||||
sink VideoSink,
|
||||
config FeedConfig,
|
||||
) (resultErr error) {
|
||||
reader, err := factory.OpenVideo(ctx, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open video: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if closeErr := reader.Close(); closeErr != nil {
|
||||
closeErr = fmt.Errorf("close video: %w", closeErr)
|
||||
resultErr = errors.Join(resultErr, closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
frame, err := reader.ReadVideo(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return fmt.Errorf("read video: %w", err)
|
||||
}
|
||||
|
||||
if err := sink.ConsumeVideo(ctx, frame); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return &videoSinkError{err: err}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package playback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeVideoFactory struct {
|
||||
reader VideoReader
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (f *fakeVideoFactory) OpenVideo(
|
||||
context.Context,
|
||||
FeedConfig,
|
||||
) (VideoReader, error) {
|
||||
f.calls++
|
||||
return f.reader, f.err
|
||||
}
|
||||
|
||||
type fakeVideoReader struct {
|
||||
frames []VideoFrame
|
||||
readErr error
|
||||
closeErr error
|
||||
readCalls int
|
||||
closed bool
|
||||
read func(context.Context) (VideoFrame, error)
|
||||
}
|
||||
|
||||
func (r *fakeVideoReader) ReadVideo(ctx context.Context) (VideoFrame, error) {
|
||||
r.readCalls++
|
||||
if r.read != nil {
|
||||
return r.read(ctx)
|
||||
}
|
||||
if len(r.frames) == 0 {
|
||||
return VideoFrame{}, r.readErr
|
||||
}
|
||||
frame := r.frames[0]
|
||||
r.frames = r.frames[1:]
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
func (r *fakeVideoReader) Close() error {
|
||||
r.closed = true
|
||||
return r.closeErr
|
||||
}
|
||||
|
||||
type fakeVideoSink struct {
|
||||
frames []VideoFrame
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *fakeVideoSink) ConsumeVideo(_ context.Context, frame VideoFrame) error {
|
||||
s.frames = append(s.frames, frame)
|
||||
return s.err
|
||||
}
|
||||
|
||||
func TestRunVideoAttemptOpenFailure(t *testing.T) {
|
||||
openErr := errors.New("open failed")
|
||||
factory := &fakeVideoFactory{err: openErr}
|
||||
sink := &fakeVideoSink{}
|
||||
|
||||
err := runVideoAttempt(
|
||||
context.Background(),
|
||||
factory,
|
||||
sink,
|
||||
FeedConfig{},
|
||||
)
|
||||
|
||||
if !errors.Is(err, openErr) {
|
||||
t.Fatalf("runVideoAttempt() 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 TestRunVideoAttemptConsumesFrameThenReturnsReadFailure(t *testing.T) {
|
||||
readErr := errors.New("read failed")
|
||||
wantFrame := VideoFrame{
|
||||
Index: 42,
|
||||
Width: 1920,
|
||||
Height: 1080,
|
||||
Stride: 5120,
|
||||
Size: 5120 * 1080,
|
||||
Invalid: false,
|
||||
Payload: []byte{1, 2, 3, 4},
|
||||
}
|
||||
reader := &fakeVideoReader{
|
||||
frames: []VideoFrame{wantFrame},
|
||||
readErr: readErr,
|
||||
}
|
||||
sink := &fakeVideoSink{}
|
||||
|
||||
err := runVideoAttempt(
|
||||
context.Background(),
|
||||
&fakeVideoFactory{reader: reader},
|
||||
sink,
|
||||
FeedConfig{},
|
||||
)
|
||||
|
||||
if !errors.Is(err, readErr) {
|
||||
t.Fatalf("runVideoAttempt() 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))
|
||||
}
|
||||
gotFrame := sink.frames[0]
|
||||
if gotFrame.Index != wantFrame.Index ||
|
||||
gotFrame.Width != wantFrame.Width ||
|
||||
gotFrame.Height != wantFrame.Height ||
|
||||
gotFrame.Stride != wantFrame.Stride ||
|
||||
gotFrame.Size != wantFrame.Size ||
|
||||
gotFrame.Invalid != wantFrame.Invalid {
|
||||
t.Errorf("consumed frame metadata = %+v, want %+v", gotFrame, wantFrame)
|
||||
}
|
||||
if len(gotFrame.Payload) == 0 {
|
||||
t.Fatal("consumed payload is empty")
|
||||
}
|
||||
if &gotFrame.Payload[0] != &wantFrame.Payload[0] {
|
||||
t.Fatal("video payload was copied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVideoAttemptSinkFailureStopsReadingAndCloses(t *testing.T) {
|
||||
sinkErr := errors.New("renderer unavailable")
|
||||
reader := &fakeVideoReader{
|
||||
frames: []VideoFrame{
|
||||
{Index: 1, Payload: []byte{1}},
|
||||
{Index: 2, Payload: []byte{2}},
|
||||
},
|
||||
}
|
||||
sink := &fakeVideoSink{err: sinkErr}
|
||||
|
||||
err := runVideoAttempt(
|
||||
context.Background(),
|
||||
&fakeVideoFactory{reader: reader},
|
||||
sink,
|
||||
FeedConfig{},
|
||||
)
|
||||
|
||||
if !errors.Is(err, sinkErr) {
|
||||
t.Fatalf("runVideoAttempt() error = %v, want %v", err, sinkErr)
|
||||
}
|
||||
var typedErr *videoSinkError
|
||||
if !errors.As(err, &typedErr) {
|
||||
t.Fatalf("runVideoAttempt() error type = %T, want *videoSinkError", err)
|
||||
}
|
||||
if reader.readCalls != 1 {
|
||||
t.Errorf("read calls = %d, want 1", reader.readCalls)
|
||||
}
|
||||
if !reader.closed {
|
||||
t.Fatal("reader was not closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVideoAttemptCanceledRead(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
reader := &fakeVideoReader{
|
||||
read: func(ctx context.Context) (VideoFrame, error) {
|
||||
cancel()
|
||||
return VideoFrame{}, ctx.Err()
|
||||
},
|
||||
}
|
||||
|
||||
err := runVideoAttempt(
|
||||
ctx,
|
||||
&fakeVideoFactory{reader: reader},
|
||||
&fakeVideoSink{},
|
||||
FeedConfig{},
|
||||
)
|
||||
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("runVideoAttempt() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if !reader.closed {
|
||||
t.Fatal("reader was not closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVideoAttemptJoinsReadAndCloseErrors(t *testing.T) {
|
||||
readErr := errors.New("read failed")
|
||||
closeErr := errors.New("close failed")
|
||||
reader := &fakeVideoReader{
|
||||
readErr: readErr,
|
||||
closeErr: closeErr,
|
||||
}
|
||||
|
||||
err := runVideoAttempt(
|
||||
context.Background(),
|
||||
&fakeVideoFactory{reader: reader},
|
||||
&fakeVideoSink{},
|
||||
FeedConfig{},
|
||||
)
|
||||
|
||||
if !errors.Is(err, readErr) {
|
||||
t.Errorf("runVideoAttempt() error does not contain read error: %v", err)
|
||||
}
|
||||
if !errors.Is(err, closeErr) {
|
||||
t.Errorf("runVideoAttempt() error does not contain close error: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user