Files
2026-08-19 19:32:14 +02:00

131 lines
4.0 KiB
Go

package player
import (
"bufio"
"bytes"
"errors"
"io"
"os/exec"
"runtime"
"testing"
"time"
)
func TestWaitCause(t *testing.T) {
ytdlErr := errors.New("yt-dlp: Sign in to confirm you're not a bot")
ffmpegErr := errors.New("ffmpeg: Invalid data found when processing input")
tests := []struct {
name string
d time.Duration
ytdl error // value sent on ytdlErr, gated by send
send bool // whether to send ytdl at all
ff error
ffSnd bool
want error
}{
// Blocking (grace) path.
{name: "ytdl error preferred over ffmpeg", d: 50 * time.Millisecond, ytdl: ytdlErr, send: true, ff: ffmpegErr, ffSnd: true, want: ytdlErr},
{name: "ffmpeg error when ytdl exits clean", d: 50 * time.Millisecond, ytdl: nil, send: true, ff: ffmpegErr, ffSnd: true, want: ffmpegErr},
{name: "both clean exit", d: 50 * time.Millisecond, ytdl: nil, send: true, ff: nil, ffSnd: true, want: nil},
{name: "ytdl error without ffmpeg report", d: 50 * time.Millisecond, ytdl: ytdlErr, send: true, ff: nil, ffSnd: false, want: ytdlErr},
{name: "neither reports before deadline", d: 50 * time.Millisecond, send: false, ffSnd: false, want: nil},
// Non-blocking poll (d <= 0).
{name: "poll ytdl error", d: 0, ytdl: ytdlErr, send: true, want: ytdlErr},
{name: "poll ffmpeg fallback after clean ytdl", d: 0, ytdl: nil, send: true, ff: ffmpegErr, ffSnd: true, want: ffmpegErr},
{name: "poll nothing pending", d: 0, send: false, ffSnd: false, want: nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ytdlCh := make(chan error, 1)
ffmpegCh := make(chan error, 1)
if tt.send {
ytdlCh <- tt.ytdl
}
if tt.ffSnd {
ffmpegCh <- tt.ff
}
y := &ytdlPipeStreamer{ytdlErr: ytdlCh, ffmpegErr: ffmpegCh}
got := y.waitCause(tt.d)
if !errors.Is(got, tt.want) {
t.Fatalf("waitCause = %v, want %v", got, tt.want)
}
})
}
}
func TestYTDLPipeErrConcurrentWithStream(t *testing.T) {
readErr := errors.New("yt-dlp PCM read failed")
y := &ytdlPipeStreamer{
reader: bufio.NewReader(&readResult{data: []byte{1}, err: readErr}),
ytdlErr: make(chan error),
ffmpegErr: make(chan error),
state: newPipeStreamState(0),
}
testPipeErrConcurrentWithStream(t, y, readErr)
}
// TestWaitCauseReturnsBeforeDeadline verifies that a present yt-dlp error is
// returned promptly rather than blocking for the full grace period waiting on
// a silent ffmpeg.
func TestWaitCauseReturnsBeforeDeadline(t *testing.T) {
ytdlCh := make(chan error, 1)
ytdlCh <- errors.New("boom")
y := &ytdlPipeStreamer{ytdlErr: ytdlCh, ffmpegErr: make(chan error, 1)}
start := time.Now()
if err := y.waitCause(2 * time.Second); err == nil {
t.Fatal("expected error, got nil")
}
if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
t.Fatalf("waitCause blocked %v waiting for ffmpeg; should return on yt-dlp error", elapsed)
}
}
func TestYTDLPipeCloseReapsBothProcesses(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses POSIX process fixture")
}
ytdlCmd := exec.Command("sleep", "30")
ffmpegCmd := exec.Command("sleep", "30")
var ytdlStderr, ffmpegStderr limitedBuffer
if err := ytdlCmd.Start(); err != nil {
t.Fatal(err)
}
if err := ffmpegCmd.Start(); err != nil {
_ = ytdlCmd.Process.Kill()
_ = ytdlCmd.Wait()
t.Fatal(err)
}
ytdlErr, ytdlDone := monitorExit(ytdlCmd, &ytdlStderr, "yt-dlp")
ffmpegErr, ffmpegDone := monitorExit(ffmpegCmd, &ffmpegStderr, "ffmpeg")
y := &ytdlPipeStreamer{
ytdlCmd: ytdlCmd,
ffmpegCmd: ffmpegCmd,
pipe: io.NopCloser(bytes.NewReader(nil)),
ytdlErr: ytdlErr,
ffmpegErr: ffmpegErr,
ytdlDone: ytdlDone,
ffmpegDone: ffmpegDone,
}
start := time.Now()
if err := y.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
if elapsed := time.Since(start); elapsed > 2*time.Second {
t.Fatalf("Close() took %v", elapsed)
}
select {
case <-ytdlDone:
default:
t.Fatal("yt-dlp process was not reaped")
}
select {
case <-ffmpegDone:
default:
t.Fatal("FFmpeg process was not reaped")
}
}