Three lifecycle controls on the dashboard, sharing one /api surface and the status feed: - Start/hold gate (#11): the watcher tracks the queue but holds processing until POST /api/start. Held by default; AV1DAE_AUTOSTART=1 restores start- on-boot. NOTE: flips the previous auto-start default. - Pause/resume the active encode (#10): SIGSTOP/SIGCONT on the ffmpeg process (libsvtav1 is in-process, so one signal suspends all its threads). Resumes exactly where it left off; the tracker excludes paused time from elapsed. - Retry a failed file (#3): POST /api/retry?file=NAME moves it from failed/ back to input/, with a base-name guard against path traversal. /status now reports running + the failed list; snapshots carry a paused flag. Verified live: held queue, retry move, traversal -> 400, and a real encode suspending to process state T on pause and S on resume. Closes #3 Closes #10 Closes #11
111 lines
3.2 KiB
Go
111 lines
3.2 KiB
Go
package status
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestScanProgress(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantOutSec, wantFPS, wantSpd float64
|
|
wantDone bool
|
|
}{
|
|
{
|
|
name: "out_time_us preferred over ms and string",
|
|
input: "frame=120\nfps=24.00\nout_time_us=5000000\nout_time_ms=9999999\nout_time=00:00:09.000000\nspeed=1.02x\nprogress=continue\n",
|
|
wantOutSec: 5, wantFPS: 24, wantSpd: 1.02, wantDone: false,
|
|
},
|
|
{
|
|
name: "out_time string fallback when no us field",
|
|
input: "fps=12\nout_time=00:01:30.500000\nspeed=0.09x\nprogress=continue\n",
|
|
wantOutSec: 90.5, wantFPS: 12, wantSpd: 0.09, wantDone: false,
|
|
},
|
|
{
|
|
name: "end block",
|
|
input: "out_time_us=7200000000\nspeed=2x\nprogress=end\n",
|
|
wantOutSec: 7200, wantFPS: 0, wantSpd: 2, wantDone: true,
|
|
},
|
|
{
|
|
name: "speed N/A leaves zero",
|
|
input: "out_time_us=1000000\nspeed=N/A\nprogress=continue\n",
|
|
wantOutSec: 1, wantFPS: 0, wantSpd: 0, wantDone: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var last ProgressSample
|
|
n := 0
|
|
if err := ScanProgress(strings.NewReader(tt.input), func(s ProgressSample) { last = s; n++ }); err != nil {
|
|
t.Fatalf("ScanProgress: %v", err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("got %d samples, want 1", n)
|
|
}
|
|
if last.OutTimeSec != tt.wantOutSec || last.FPS != tt.wantFPS || last.Speed != tt.wantSpd || last.Done != tt.wantDone {
|
|
t.Errorf("got %+v, want out=%v fps=%v spd=%v done=%v", last, tt.wantOutSec, tt.wantFPS, tt.wantSpd, tt.wantDone)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScanProgressMultipleBlocks(t *testing.T) {
|
|
in := "out_time_us=1000000\nspeed=1x\nprogress=continue\nout_time_us=2000000\nspeed=1x\nprogress=end\n"
|
|
var got []ProgressSample
|
|
if err := ScanProgress(strings.NewReader(in), func(s ProgressSample) { got = append(got, s) }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("got %d blocks, want 2", len(got))
|
|
}
|
|
if got[0].OutTimeSec != 1 || got[1].OutTimeSec != 2 || got[0].Done || !got[1].Done {
|
|
t.Errorf("blocks = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestSnapshotPercentAndETA(t *testing.T) {
|
|
tr := New()
|
|
tr.Begin("episode.mkv")
|
|
tr.SetTotal(100)
|
|
tr.Update(25, 10, 0.5) // 25% done, 75s left at 0.5x -> 150s ETA
|
|
s := tr.Snapshot()
|
|
if s.Percent != 25 {
|
|
t.Errorf("Percent = %v, want 25", s.Percent)
|
|
}
|
|
if s.ETASec != 150 {
|
|
t.Errorf("ETASec = %v, want 150", s.ETASec)
|
|
}
|
|
if s.File != "episode.mkv" || s.Phase != PhaseProbing {
|
|
t.Errorf("File/Phase = %q/%q", s.File, s.Phase)
|
|
}
|
|
}
|
|
|
|
func TestSetPausedFlag(t *testing.T) {
|
|
tr := New()
|
|
tr.Begin("x.mkv")
|
|
if tr.Snapshot().Paused {
|
|
t.Fatal("should not start paused")
|
|
}
|
|
tr.SetPaused(true)
|
|
if !tr.Snapshot().Paused {
|
|
t.Error("expected paused after SetPaused(true)")
|
|
}
|
|
tr.SetPaused(true) // idempotent — must not double-count
|
|
tr.SetPaused(false)
|
|
if tr.Snapshot().Paused {
|
|
t.Error("expected not paused after SetPaused(false)")
|
|
}
|
|
tr.Idle()
|
|
if tr.Snapshot().Paused {
|
|
t.Error("Idle should clear paused")
|
|
}
|
|
}
|
|
|
|
func TestSnapshotIdleNoDivByZero(t *testing.T) {
|
|
s := New().Snapshot() // no Begin, totalSec 0
|
|
if s.Percent != 0 || s.ETASec != 0 || s.Phase != PhaseIdle {
|
|
t.Errorf("idle snapshot = %+v", s)
|
|
}
|
|
}
|