commit 3bdbe9d9e5d2e907bf64e2d33492e9a4e5648f9a
parent 87ea90df507740bf93d4cf2840953b0b38977098
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Thu, 17 Sep 2026 11:58:20 -0400
test: cover the ffmpeg fallbacks when ffmpeg is installed
FFmpegLoad, FFmpegDecode and FFmpegPlay had no tests, which is how a
mistyped ffplay flag shipped. They now run wherever ffmpeg is on PATH,
including Windows, so the fallback is checked without a Linux machine.
Load and Decode must agree exactly with each other and match the
ffmpeg-produced reference; FFmpegPlay must block for the file's duration.
Diffstat:
1 file changed, 70 insertions(+), 0 deletions(-)
diff --git a/internal/test/ffmpeg_test.go b/internal/test/ffmpeg_test.go
@@ -0,0 +1,70 @@
+package test
+
+import (
+ "bytes"
+ "os/exec"
+ "testing"
+ "time"
+
+ "git.sr.ht/~jackmordaunt/nativeaudio"
+)
+
+// requireTools skips the test unless every named program is on PATH.
+func requireTools(t *testing.T, names ...string) {
+ t.Helper()
+ for _, n := range names {
+ if _, err := exec.LookPath(n); err != nil {
+ t.Skipf("%s not installed", n)
+ }
+ }
+}
+
+// TestFFmpegLoadAndDecode exercises the ffmpeg fallback on every
+// platform that has ffmpeg, regardless of which native decoder is in use.
+// The reference PCM was produced by ffmpeg, so output should match within
+// the same tolerance as the native decoders, and Load and Decode must
+// agree with each other exactly.
+func TestFFmpegLoadAndDecode(t *testing.T) {
+ requireTools(t, "ffmpeg", "ffprobe")
+ want := nativeaudio.Format{SampleRate: 44100, Channels: 2, BytesPerSample: 2}
+
+ loaded, f, err := nativeaudio.FFmpegLoad("compressed.m4a")
+ if err != nil {
+ t.Fatalf("FFmpegLoad: %v", err)
+ }
+ if f != want {
+ t.Fatalf("FFmpegLoad format: want %+v, got %+v", want, f)
+ }
+ if !bytes.Equal(loaded, uncompressed) && !equal(t, loaded, uncompressed) {
+ t.Fatal("FFmpegLoad output does not match reference")
+ }
+
+ decoded, f, err := nativeaudio.FFmpegDecode(compressed)
+ if err != nil {
+ t.Fatalf("FFmpegDecode: %v", err)
+ }
+ if f != want {
+ t.Fatalf("FFmpegDecode format: want %+v, got %+v", want, f)
+ }
+ if !bytes.Equal(decoded, loaded) {
+ t.Fatal("FFmpegDecode output differs from FFmpegLoad output for the same data")
+ }
+}
+
+// TestFFmpegPlay plays one second of silence through ffplay and checks
+// the call blocks for at least that long. A mistyped flag used to make
+// ffplay exit immediately with an error.
+func TestFFmpegPlay(t *testing.T) {
+ requireTools(t, "ffplay")
+ const seconds = 1
+ path := writeTemp(t, "silence.wav", silentWAV(44100, 2, seconds))
+ start := time.Now()
+ if err := nativeaudio.FFmpegPlay(path); err != nil {
+ t.Fatalf("FFmpegPlay: %v", err)
+ }
+ elapsed := time.Since(start)
+ t.Logf("ffplay took %v", elapsed.Round(time.Millisecond))
+ if min := time.Duration(seconds) * time.Second * 9 / 10; elapsed < min {
+ t.Errorf("FFmpegPlay returned after %v, want at least %v", elapsed, min)
+ }
+}