unarr/internal/engine/watch_reporter_test.go
Deivid Soto 4d7362a567 fix(daemon): cancel watch reporter on stream switch and re-notify ready
- Register WatchReporter cancel funcs in streamRegistry so they get
  cancelled when switching to a different stream (prevents goroutine leak)
- Re-notify streamReady when the server is already serving the requested
  task (handles duplicate stream requests from the web UI)
- Rewrite tests for byte-based tracking semantics, remove dead
  parseRangeStart tests
2026-04-07 23:30:53 +02:00

136 lines
3.6 KiB
Go

package engine
import (
"context"
"io"
"net/http"
"os"
"testing"
)
// ---------------------------------------------------------------------------
// StreamServer.EstimatedProgress
// ---------------------------------------------------------------------------
func TestEstimatedProgress_NoFile(t *testing.T) {
ss := &StreamServer{}
pos, dur := ss.EstimatedProgress()
if pos != 0 || dur != 0 {
t.Errorf("expected (0, 0), got (%d, %d)", pos, dur)
}
}
func TestEstimatedProgress_HalfWay(t *testing.T) {
ss := &StreamServer{}
ss.totalFileSize.Store(1000)
ss.maxByteOffset.Store(500)
pos, _ := ss.EstimatedProgress()
if pos != 50 {
t.Errorf("expected pct=50, got %d", pos)
}
}
func TestEstimatedProgress_CapsAt100(t *testing.T) {
ss := &StreamServer{}
ss.totalFileSize.Store(1000)
ss.maxByteOffset.Store(1500)
pos, _ := ss.EstimatedProgress()
if pos != 100 {
t.Errorf("expected pct=100, got %d", pos)
}
}
// ---------------------------------------------------------------------------
// maxByteOffset only increases (simulated Range tracking)
// ---------------------------------------------------------------------------
func TestMaxByteOffsetNeverRegresses(t *testing.T) {
ss := &StreamServer{}
ss.totalFileSize.Store(10000)
offsets := []int64{0, 2000, 5000, 3000, 8000, 4000}
for _, off := range offsets {
for {
cur := ss.maxByteOffset.Load()
if off <= cur || ss.maxByteOffset.CompareAndSwap(cur, off) {
break
}
}
}
if ss.maxByteOffset.Load() != 8000 {
t.Errorf("expected 8000, got %d", ss.maxByteOffset.Load())
}
}
// ---------------------------------------------------------------------------
// End-to-end: real HTTP server with Range requests
// ---------------------------------------------------------------------------
func TestStreamServerByteTracking(t *testing.T) {
// Create temp file (10 KB)
tmpFile := t.TempDir() + "/test.mp4"
data := make([]byte, 10240)
for i := range data {
data[i] = byte(i % 256)
}
if err := os.WriteFile(tmpFile, data, 0o644); err != nil {
t.Fatal(err)
}
srv := NewStreamServer(0)
srv.disableUPnP = true
ctx := context.Background()
if err := srv.Listen(ctx); err != nil {
t.Fatalf("listen: %v", err)
}
defer srv.Shutdown(ctx)
srv.SetFile(NewDiskFileProvider(tmpFile), "test-task")
url := srv.URL()
// 1. Full GET — reads all bytes, maxByteOffset reaches file size
resp, err := http.Get(url)
if err != nil {
t.Fatalf("GET: %v", err)
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if srv.maxByteOffset.Load() != 10240 {
t.Errorf("full read: expected 10240, got %d", srv.maxByteOffset.Load())
}
// 2. Reset and verify progress after partial read via Range
srv.SetFile(NewDiskFileProvider(tmpFile), "test-task-2")
if srv.maxByteOffset.Load() != 0 {
t.Errorf("after reset: expected 0, got %d", srv.maxByteOffset.Load())
}
// Range request reads from offset 5000 to end (5240 bytes)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Range", "bytes=5000-")
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("Range GET: %v", err)
}
if resp.StatusCode != http.StatusPartialContent {
t.Errorf("expected 206, got %d", resp.StatusCode)
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
// The reader reads 5240 bytes (from offset 5000 to 10240).
// maxByteOffset tracks the read position, which ends at 10240.
got := srv.maxByteOffset.Load()
if got != 10240 {
t.Errorf("after range read: expected 10240, got %d", got)
}
// 3. Verify progress reaches 100%
pos, _ := srv.EstimatedProgress()
if pos != 100 {
t.Errorf("expected pct=100, got %d", pos)
}
}