328 lines
9.2 KiB
Go
328 lines
9.2 KiB
Go
package whisperx
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"mime"
|
|
"mime/multipart"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMultipartBodyReplayUsesIdenticalFraming(t *testing.T) {
|
|
factory := newTestMultipartBodyFactory(t, context.Background(), "audio-data")
|
|
defer factory.Close()
|
|
|
|
original, err := factory.NewBody()
|
|
if err != nil {
|
|
t.Fatalf("NewBody() original error = %v", err)
|
|
}
|
|
replay, err := factory.NewBody()
|
|
if err != nil {
|
|
t.Fatalf("NewBody() replay error = %v", err)
|
|
}
|
|
originalBytes, err := io.ReadAll(original)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll(original) error = %v", err)
|
|
}
|
|
replayBytes, err := io.ReadAll(replay)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll(replay) error = %v", err)
|
|
}
|
|
if !bytes.Equal(originalBytes, replayBytes) {
|
|
t.Fatal("original and replay multipart bytes differ")
|
|
}
|
|
|
|
parts := parseMultipartParts(t, factory.contentType, replayBytes)
|
|
if len(parts) != 2 {
|
|
t.Fatalf("part count = %d, want 2", len(parts))
|
|
}
|
|
if parts[0].name != "file" || parts[0].filename != "audio.flac" || string(parts[0].data) != "audio-data" {
|
|
t.Fatalf("file part = %#v, want file/audio.flac with exact audio", parts[0])
|
|
}
|
|
if parts[1].name != "language" || string(parts[1].data) != "en" {
|
|
t.Fatalf("language part = %#v, want language=en", parts[1])
|
|
}
|
|
}
|
|
|
|
func TestMultipartBodyReplayRestartsAfterPartialOriginal(t *testing.T) {
|
|
factory := newTestMultipartBodyFactory(t, context.Background(), "audio-data")
|
|
defer factory.Close()
|
|
|
|
original, err := factory.NewBody()
|
|
if err != nil {
|
|
t.Fatalf("NewBody() original error = %v", err)
|
|
}
|
|
partial := make([]byte, len(factory.prefix)+3)
|
|
if _, err := io.ReadFull(original, partial); err != nil {
|
|
t.Fatalf("ReadFull(original) error = %v", err)
|
|
}
|
|
if err := original.Close(); err != nil {
|
|
t.Fatalf("Close(original) error = %v", err)
|
|
}
|
|
|
|
replay, err := factory.NewBody()
|
|
if err != nil {
|
|
t.Fatalf("NewBody() replay error = %v", err)
|
|
}
|
|
replayBytes, err := io.ReadAll(replay)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll(replay) error = %v", err)
|
|
}
|
|
parts := parseMultipartParts(t, factory.contentType, replayBytes)
|
|
if got := string(parts[0].data); got != "audio-data" {
|
|
t.Fatalf("replayed audio = %q, want complete payload", got)
|
|
}
|
|
if err := factory.SourceError(false); err != nil {
|
|
t.Fatalf("SourceError(false) = %v, want abandoned original ignored", err)
|
|
}
|
|
}
|
|
|
|
func TestMultipartBodySimultaneousBodiesUseIndependentSources(t *testing.T) {
|
|
var opens atomic.Int32
|
|
var closes atomic.Int32
|
|
factory, err := newMultipartBodyFactory(context.Background(), "audio.flac", "en", func(string) (io.ReadCloser, error) {
|
|
opens.Add(1)
|
|
return &countedReadCloser{Reader: strings.NewReader("audio-data"), closes: &closes}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("newMultipartBodyFactory() error = %v", err)
|
|
}
|
|
|
|
first, err := factory.NewBody()
|
|
if err != nil {
|
|
t.Fatalf("NewBody() first error = %v", err)
|
|
}
|
|
second, err := factory.NewBody()
|
|
if err != nil {
|
|
t.Fatalf("NewBody() second error = %v", err)
|
|
}
|
|
var firstBytes, secondBytes []byte
|
|
var firstErr, secondErr error
|
|
var group sync.WaitGroup
|
|
group.Add(2)
|
|
go func() {
|
|
defer group.Done()
|
|
firstBytes, firstErr = io.ReadAll(first)
|
|
}()
|
|
go func() {
|
|
defer group.Done()
|
|
secondBytes, secondErr = io.ReadAll(second)
|
|
}()
|
|
group.Wait()
|
|
if firstErr != nil || secondErr != nil {
|
|
t.Fatalf("concurrent reads returned (%v, %v)", firstErr, secondErr)
|
|
}
|
|
if !bytes.Equal(firstBytes, secondBytes) {
|
|
t.Fatal("independent body bytes differ")
|
|
}
|
|
if got := opens.Load(); got != 2 {
|
|
t.Fatalf("audio opens = %d, want 2", got)
|
|
}
|
|
if err := factory.Close(); err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
if err := factory.Close(); err != nil {
|
|
t.Fatalf("second Close() error = %v", err)
|
|
}
|
|
if got := closes.Load(); got != 2 {
|
|
t.Fatalf("audio closes = %d, want 2", got)
|
|
}
|
|
}
|
|
|
|
func TestMultipartBodyReportsSourceFailures(t *testing.T) {
|
|
t.Run("read", func(t *testing.T) {
|
|
sourceErr := errors.New("read source")
|
|
factory, err := newMultipartBodyFactory(context.Background(), "audio.flac", "en", func(string) (io.ReadCloser, error) {
|
|
return &failingReadCloser{first: []byte("partial"), err: sourceErr}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("newMultipartBodyFactory() error = %v", err)
|
|
}
|
|
body, err := factory.NewBody()
|
|
if err != nil {
|
|
t.Fatalf("NewBody() error = %v", err)
|
|
}
|
|
if _, err := io.ReadAll(body); !errors.Is(err, sourceErr) {
|
|
t.Fatalf("ReadAll() error = %v, want source failure", err)
|
|
}
|
|
_ = body.Close()
|
|
if err := factory.SourceError(false); !errors.Is(err, sourceErr) {
|
|
t.Fatalf("SourceError(false) = %v, want source failure", err)
|
|
}
|
|
})
|
|
|
|
t.Run("close after complete read", func(t *testing.T) {
|
|
closeErr := errors.New("close source")
|
|
factory, err := newMultipartBodyFactory(context.Background(), "audio.flac", "en", func(string) (io.ReadCloser, error) {
|
|
return &errorCloseReadCloser{Reader: strings.NewReader("audio-data"), err: closeErr}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("newMultipartBodyFactory() error = %v", err)
|
|
}
|
|
body, err := factory.NewBody()
|
|
if err != nil {
|
|
t.Fatalf("NewBody() error = %v", err)
|
|
}
|
|
if _, err := io.ReadAll(body); err != nil {
|
|
t.Fatalf("ReadAll() error = %v", err)
|
|
}
|
|
if err := body.Close(); !errors.Is(err, closeErr) {
|
|
t.Fatalf("Close() error = %v, want source close failure", err)
|
|
}
|
|
if err := factory.SourceError(false); !errors.Is(err, closeErr) {
|
|
t.Fatalf("SourceError(false) = %v, want source close failure", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMultipartBodyCloseAndCancelReleaseBlockedSource(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
release func(context.CancelFunc, io.ReadCloser)
|
|
}{
|
|
{name: "close", release: func(_ context.CancelFunc, body io.ReadCloser) { _ = body.Close() }},
|
|
{name: "cancel", release: func(cancel context.CancelFunc, _ io.ReadCloser) { cancel() }},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
source := newSignalingBlockedReadCloser()
|
|
factory, err := newMultipartBodyFactory(ctx, "audio.flac", "en", func(string) (io.ReadCloser, error) {
|
|
return source, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("newMultipartBodyFactory() error = %v", err)
|
|
}
|
|
defer factory.Close()
|
|
body, err := factory.NewBody()
|
|
if err != nil {
|
|
t.Fatalf("NewBody() error = %v", err)
|
|
}
|
|
readDone := make(chan error, 1)
|
|
go func() {
|
|
_, err := io.ReadAll(body)
|
|
readDone <- err
|
|
}()
|
|
select {
|
|
case <-source.blocked:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("audio source did not block")
|
|
}
|
|
test.release(cancel, body)
|
|
select {
|
|
case <-source.closed:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("audio source was not closed")
|
|
}
|
|
select {
|
|
case <-readDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("blocked body read did not return")
|
|
}
|
|
if test.name == "cancel" {
|
|
if _, err := factory.NewBody(); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("NewBody() after cancellation error = %v, want context.Canceled", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func newTestMultipartBodyFactory(t *testing.T, ctx context.Context, audio string) *multipartBodyFactory {
|
|
t.Helper()
|
|
factory, err := newMultipartBodyFactory(ctx, "audio.flac", "en", func(string) (io.ReadCloser, error) {
|
|
return io.NopCloser(strings.NewReader(audio)), nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("newMultipartBodyFactory() error = %v", err)
|
|
}
|
|
return factory
|
|
}
|
|
|
|
type multipartTestPart struct {
|
|
name string
|
|
filename string
|
|
data []byte
|
|
}
|
|
|
|
func parseMultipartParts(t *testing.T, contentType string, body []byte) []multipartTestPart {
|
|
t.Helper()
|
|
mediaType, params, err := mime.ParseMediaType(contentType)
|
|
if err != nil {
|
|
t.Fatalf("ParseMediaType() error = %v", err)
|
|
}
|
|
if mediaType != "multipart/form-data" {
|
|
t.Fatalf("media type = %q, want multipart/form-data", mediaType)
|
|
}
|
|
reader := multipart.NewReader(bytes.NewReader(body), params["boundary"])
|
|
var parts []multipartTestPart
|
|
for {
|
|
part, err := reader.NextPart()
|
|
if errors.Is(err, io.EOF) {
|
|
return parts
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("NextPart() error = %v", err)
|
|
}
|
|
data, err := io.ReadAll(part)
|
|
if err != nil {
|
|
t.Fatalf("ReadAll(part %q) error = %v", part.FormName(), err)
|
|
}
|
|
parts = append(parts, multipartTestPart{name: part.FormName(), filename: part.FileName(), data: data})
|
|
}
|
|
}
|
|
|
|
type countedReadCloser struct {
|
|
io.Reader
|
|
closes *atomic.Int32
|
|
once sync.Once
|
|
}
|
|
|
|
func (reader *countedReadCloser) Close() error {
|
|
reader.once.Do(func() { reader.closes.Add(1) })
|
|
return nil
|
|
}
|
|
|
|
type errorCloseReadCloser struct {
|
|
io.Reader
|
|
err error
|
|
}
|
|
|
|
func (reader *errorCloseReadCloser) Close() error { return reader.err }
|
|
|
|
type signalingBlockedReadCloser struct {
|
|
blocked chan struct{}
|
|
closed chan struct{}
|
|
once sync.Once
|
|
read bool
|
|
}
|
|
|
|
func newSignalingBlockedReadCloser() *signalingBlockedReadCloser {
|
|
return &signalingBlockedReadCloser{blocked: make(chan struct{}), closed: make(chan struct{})}
|
|
}
|
|
|
|
func (reader *signalingBlockedReadCloser) Read(buffer []byte) (int, error) {
|
|
if !reader.read {
|
|
reader.read = true
|
|
return copy(buffer, "audio-data"), nil
|
|
}
|
|
reader.once.Do(func() { close(reader.blocked) })
|
|
<-reader.closed
|
|
return 0, errors.New("audio source closed")
|
|
}
|
|
|
|
func (reader *signalingBlockedReadCloser) Close() error {
|
|
select {
|
|
case <-reader.closed:
|
|
default:
|
|
close(reader.closed)
|
|
}
|
|
return nil
|
|
}
|