124 lines
3.6 KiB
Go
124 lines
3.6 KiB
Go
|
|
package engine
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/torrentclaw/unarr/internal/agent"
|
||
|
|
)
|
||
|
|
|
||
|
|
// fakePersister is an in-memory taskPersister for asserting manager↔store calls
|
||
|
|
// without touching disk.
|
||
|
|
type fakePersister struct {
|
||
|
|
mu sync.Mutex
|
||
|
|
tasks map[string]bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func newFakePersister() *fakePersister { return &fakePersister{tasks: map[string]bool{}} }
|
||
|
|
func (f *fakePersister) Add(t agent.Task) { f.mu.Lock(); f.tasks[t.ID] = true; f.mu.Unlock() }
|
||
|
|
func (f *fakePersister) Remove(id string) { f.mu.Lock(); delete(f.tasks, id); f.mu.Unlock() }
|
||
|
|
func (f *fakePersister) has(id string) bool { f.mu.Lock(); defer f.mu.Unlock(); return f.tasks[id] }
|
||
|
|
|
||
|
|
func newResumeManager(t *testing.T, p taskPersister) (*Manager, context.Context, context.CancelFunc) {
|
||
|
|
t.Helper()
|
||
|
|
reporter := NewProgressReporter(agent.NewClient("http://localhost", "test", "test"), time.Hour)
|
||
|
|
mgr := NewManager(
|
||
|
|
ManagerConfig{MaxConcurrent: 2, OutputDir: t.TempDir()},
|
||
|
|
reporter,
|
||
|
|
&slowMockDownloader{method: MethodTorrent},
|
||
|
|
)
|
||
|
|
mgr.SetTaskStore(p)
|
||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
|
go reporter.Run(ctx)
|
||
|
|
return mgr, ctx, cancel
|
||
|
|
}
|
||
|
|
|
||
|
|
// dlTask builds a download task. IDs mirror production (UUID-length); the engine
|
||
|
|
// logs task.ID[:8] in several places, so sub-8-char ids would panic — not a real
|
||
|
|
// case since the web always sends UUIDs.
|
||
|
|
func dlTask(id string) agent.Task {
|
||
|
|
return agent.Task{
|
||
|
|
ID: "task-uuid-" + id, // ≥ 8 chars like a real dispatch id
|
||
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
||
|
|
Title: "Resume " + id,
|
||
|
|
PreferredMethod: "torrent",
|
||
|
|
Mode: "download",
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestManager_SubmitDedupes(t *testing.T) {
|
||
|
|
mgr, ctx, cancel := newResumeManager(t, newFakePersister())
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
task := dlTask("dup-1")
|
||
|
|
mgr.Submit(ctx, task)
|
||
|
|
mgr.Submit(ctx, task) // duplicate id — must not launch a second download
|
||
|
|
|
||
|
|
if n := mgr.ActiveCount(); n != 1 {
|
||
|
|
t.Errorf("ActiveCount = %d after duplicate submit, want 1", n)
|
||
|
|
}
|
||
|
|
cancel()
|
||
|
|
mgr.Wait()
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestManager_PersistsDownloadAndRemovesOnTerminal(t *testing.T) {
|
||
|
|
p := newFakePersister()
|
||
|
|
mgr, ctx, cancel := newResumeManager(t, p)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
task := dlTask("t1")
|
||
|
|
mgr.Submit(ctx, task)
|
||
|
|
if !p.has(task.ID) {
|
||
|
|
t.Fatal("download not persisted to the resume store on submit")
|
||
|
|
}
|
||
|
|
|
||
|
|
// A genuine terminal (user cancel, not shutdown) must remove it.
|
||
|
|
mgr.CancelTask(task.ID)
|
||
|
|
mgr.Wait()
|
||
|
|
if p.has(task.ID) {
|
||
|
|
t.Error("task still in resume store after a genuine terminal — should be removed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestManager_KeepsStoreEntryOnShutdown(t *testing.T) {
|
||
|
|
p := newFakePersister()
|
||
|
|
mgr, ctx, cancel := newResumeManager(t, p)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
task := dlTask("s1")
|
||
|
|
mgr.Submit(ctx, task)
|
||
|
|
if !p.has(task.ID) {
|
||
|
|
t.Fatal("download not persisted on submit")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Shutdown interrupts the in-flight download — the entry must SURVIVE so the
|
||
|
|
// daemon re-submits and resumes it next start.
|
||
|
|
// Shutdown cancels the task contexts itself then waits, so once it returns
|
||
|
|
// the interrupted task's recordFinished has run (and must have skipped the
|
||
|
|
// removal because shuttingDown is set) — no sleep/poll needed.
|
||
|
|
shutCtx, sc := context.WithTimeout(context.Background(), 5*time.Second)
|
||
|
|
defer sc()
|
||
|
|
mgr.Shutdown(shutCtx)
|
||
|
|
|
||
|
|
if !p.has(task.ID) {
|
||
|
|
t.Error("task removed from resume store on shutdown — it would not resume")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestManager_DoesNotPersistStreamTasks(t *testing.T) {
|
||
|
|
p := newFakePersister()
|
||
|
|
mgr, ctx, cancel := newResumeManager(t, p)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
task := dlTask("stream-1")
|
||
|
|
task.Mode = "stream"
|
||
|
|
mgr.Submit(ctx, task)
|
||
|
|
if p.has(task.ID) {
|
||
|
|
t.Error("stream task persisted to resume store — only downloads should be")
|
||
|
|
}
|
||
|
|
cancel()
|
||
|
|
mgr.Wait()
|
||
|
|
}
|