Red: package audio does not exist. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
130 lines
4.2 KiB
Go
130 lines
4.2 KiB
Go
package audio_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"git.d-ma.be/mathias/tapir/internal/adapters/audio"
|
|
"git.d-ma.be/mathias/tapir/internal/uploads"
|
|
)
|
|
|
|
func TestParseProbe(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
json string
|
|
want uploads.Probe
|
|
}{
|
|
{
|
|
name: "audio-only m4a",
|
|
json: `{"streams":[{"codec_type":"audio","disposition":{"attached_pic":0}}],"format":{"duration":"2701.5"}}`,
|
|
want: uploads.Probe{Duration: 2701*time.Second + 500*time.Millisecond, HasAudio: true},
|
|
},
|
|
{
|
|
name: "mp4 with a real video stream",
|
|
json: `{"streams":[{"codec_type":"video","disposition":{"attached_pic":0}},{"codec_type":"audio"}],"format":{"duration":"240.0"}}`,
|
|
want: uploads.Probe{Duration: 240 * time.Second, HasAudio: true, HasVideo: true},
|
|
},
|
|
{
|
|
name: "mp3 whose only video stream is attached cover art is NOT video",
|
|
json: `{"streams":[{"codec_type":"audio"},{"codec_type":"video","disposition":{"attached_pic":1}}],"format":{"duration":"60"}}`,
|
|
want: uploads.Probe{Duration: 60 * time.Second, HasAudio: true},
|
|
},
|
|
{
|
|
name: "no streams at all",
|
|
json: `{"streams":[],"format":{"duration":"10"}}`,
|
|
want: uploads.Probe{Duration: 10 * time.Second},
|
|
},
|
|
{
|
|
name: "duration absent means zero, which Validate rejects as unreadable",
|
|
json: `{"streams":[{"codec_type":"audio"}],"format":{}}`,
|
|
want: uploads.Probe{HasAudio: true},
|
|
},
|
|
{
|
|
name: "duration reported as N/A means zero",
|
|
json: `{"streams":[{"codec_type":"audio"}],"format":{"duration":"N/A"}}`,
|
|
want: uploads.Probe{HasAudio: true},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := audio.ParseProbe([]byte(tt.json))
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseProbe_GarbageIsAnError(t *testing.T) {
|
|
_, err := audio.ParseProbe([]byte("not json"))
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
// stub writes an executable shell script standing in for ffprobe. This is a fake
|
|
// at the process boundary: the real binary is not on every host (it is not on
|
|
// koala), and what is under test is how Probe drives and interprets a process.
|
|
func stub(t *testing.T, body string) string {
|
|
t.Helper()
|
|
p := filepath.Join(t.TempDir(), "ffprobe")
|
|
require.NoError(t, os.WriteFile(p, []byte("#!/bin/sh\n"+body+"\n"), 0o755))
|
|
return p
|
|
}
|
|
|
|
func TestFFprobeProbe_ParsesWhatTheProcessPrints(t *testing.T) {
|
|
bin := stub(t, `echo '{"streams":[{"codec_type":"audio"}],"format":{"duration":"90"}}'`)
|
|
|
|
got, err := (&audio.FFprobe{Bin: bin}).Probe(context.Background(), "/ignored")
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uploads.Probe{Duration: 90 * time.Second, HasAudio: true}, got)
|
|
}
|
|
|
|
func TestFFprobeProbe_PassesTheFilePathAsTheLastArgument(t *testing.T) {
|
|
out := filepath.Join(t.TempDir(), "args")
|
|
bin := stub(t, `for a; do last="$a"; done; printf '%s' "$last" > `+out+`
|
|
echo '{"streams":[{"codec_type":"audio"}],"format":{"duration":"1"}}'`)
|
|
|
|
_, err := (&audio.FFprobe{Bin: bin}).Probe(context.Background(), "/spool/upload-123")
|
|
|
|
require.NoError(t, err)
|
|
got, err := os.ReadFile(out)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "/spool/upload-123", string(got))
|
|
}
|
|
|
|
func TestFFprobeProbe_NonzeroExitMeansTheFileIsUnreadable(t *testing.T) {
|
|
bin := stub(t, `echo "Invalid data found when processing input" >&2; exit 1`)
|
|
|
|
_, err := (&audio.FFprobe{Bin: bin}).Probe(context.Background(), "/x")
|
|
|
|
assert.ErrorIs(t, err, uploads.ErrUnreadable)
|
|
}
|
|
|
|
func TestFFprobeProbe_MissingBinaryIsOurFaultNotAnUnreadableFile(t *testing.T) {
|
|
_, err := (&audio.FFprobe{Bin: filepath.Join(t.TempDir(), "no-such-ffprobe")}).Probe(context.Background(), "/x")
|
|
|
|
require.Error(t, err)
|
|
assert.False(t, errors.Is(err, uploads.ErrUnreadable),
|
|
"a missing ffprobe must not be reported to the user as their file being bad")
|
|
}
|
|
|
|
func TestFFprobeProbe_StopsWhenTheContextIsCancelled(t *testing.T) {
|
|
bin := stub(t, `sleep 30`)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
start := time.Now()
|
|
_, err := (&audio.FFprobe{Bin: bin}).Probe(ctx, "/x")
|
|
|
|
require.Error(t, err)
|
|
assert.Less(t, time.Since(start), 5*time.Second)
|
|
assert.False(t, errors.Is(err, uploads.ErrUnreadable), "a timeout is not a verdict on the file")
|
|
}
|