- Replace `upgrade` stub with real command (alias for `self-update`) - Also register `update` as alias: `unarr update` works too - Rewrite `status` to show full config, disk usage, daemon state, and update availability with colored sections - Add version check cache (1h TTL) so `status` is instant on repeat runs - Guard against division by zero on empty filesystems - Guard against negative durations from clock skew - Guard against stale PID via heartbeat recency check (2 min) - Add comprehensive test coverage across agent, engine, upgrade, usenet, arr, library, mediaserver, and UI packages - Improve Makefile coverage target to exclude cmd/ glue code - Fix stream handler resource cleanup and ffprobe error handling
306 lines
7.8 KiB
Go
306 lines
7.8 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/torrentclaw/unarr/internal/agent"
|
|
)
|
|
|
|
func TestManagerSubmitAndWait(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
|
|
dl := &mockDownloader{method: MethodTorrent, available: true}
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 2,
|
|
OutputDir: t.TempDir(),
|
|
}, reporter, dl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
go reporter.Run(ctx)
|
|
|
|
mgr.Submit(ctx, agent.Task{
|
|
ID: "test-task-1",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Test Movie",
|
|
PreferredMethod: "torrent",
|
|
})
|
|
|
|
mgr.Wait()
|
|
|
|
// Task should have been processed (completed or failed depending on verify)
|
|
// Since mock returns a file that doesn't exist, it may fail at verify
|
|
// This is expected — we're testing the pipeline works
|
|
}
|
|
|
|
func TestManagerHasCapacity(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
|
|
mgr := NewManager(ManagerConfig{MaxConcurrent: 2}, reporter)
|
|
|
|
if !mgr.HasCapacity() {
|
|
t.Error("new manager should have capacity")
|
|
}
|
|
}
|
|
|
|
func TestManagerActiveCount(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
|
|
mgr := NewManager(ManagerConfig{MaxConcurrent: 3}, reporter)
|
|
|
|
if mgr.ActiveCount() != 0 {
|
|
t.Errorf("ActiveCount = %d, want 0", mgr.ActiveCount())
|
|
}
|
|
}
|
|
|
|
func TestManagerShutdown(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
|
|
dl := &mockDownloader{method: MethodTorrent, available: true}
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 1,
|
|
OutputDir: t.TempDir(),
|
|
}, reporter, dl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
mgr.Shutdown(ctx)
|
|
// Should not hang
|
|
}
|
|
|
|
func TestManagerDefaultConcurrency(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
mgr := NewManager(ManagerConfig{MaxConcurrent: 0}, reporter)
|
|
if cap(mgr.sem) != 3 {
|
|
t.Errorf("default MaxConcurrent should be 3, got %d", cap(mgr.sem))
|
|
}
|
|
}
|
|
|
|
func TestManagerGetTask(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
mgr := NewManager(ManagerConfig{MaxConcurrent: 2}, reporter)
|
|
|
|
// No task added
|
|
if task := mgr.GetTask("nonexistent"); task != nil {
|
|
t.Error("expected nil for nonexistent task")
|
|
}
|
|
}
|
|
|
|
func TestManagerActiveTasks(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
mgr := NewManager(ManagerConfig{MaxConcurrent: 2}, reporter)
|
|
|
|
tasks := mgr.ActiveTasks()
|
|
if len(tasks) != 0 {
|
|
t.Errorf("expected 0 active tasks, got %d", len(tasks))
|
|
}
|
|
}
|
|
|
|
func TestManagerSubmitCompletesWithValidFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Create a file that verify() will accept
|
|
filePath := dir + "/movie.mkv"
|
|
os.WriteFile(filePath, make([]byte, 1024), 0o644)
|
|
|
|
reporter := &mockStatusReporter{}
|
|
pr := &ProgressReporter{
|
|
reporter: reporter,
|
|
interval: 100 * time.Millisecond,
|
|
latest: make(map[string]*Task),
|
|
lastReported: make(map[string]TaskStatus),
|
|
}
|
|
|
|
dl := &resultMockDownloader{
|
|
method: MethodTorrent,
|
|
result: &Result{
|
|
FilePath: filePath,
|
|
FileName: "movie.mkv",
|
|
Method: MethodTorrent,
|
|
Size: 1024,
|
|
},
|
|
}
|
|
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 2,
|
|
OutputDir: dir,
|
|
}, pr, dl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
go pr.Run(ctx)
|
|
|
|
mgr.Submit(ctx, agent.Task{
|
|
ID: "task-complete-test1",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Test Movie",
|
|
PreferredMethod: "torrent",
|
|
})
|
|
|
|
mgr.Wait()
|
|
cancel()
|
|
|
|
// Task should have completed successfully
|
|
// (we can't check directly since it's removed from active map after processing)
|
|
}
|
|
|
|
func TestManagerCancelTask(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
|
|
dl := &slowMockDownloader{method: MethodTorrent}
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 2,
|
|
OutputDir: t.TempDir(),
|
|
}, reporter, dl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
go reporter.Run(ctx)
|
|
|
|
mgr.Submit(ctx, agent.Task{
|
|
ID: "task-cancel-test12",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Cancel Me",
|
|
PreferredMethod: "torrent",
|
|
})
|
|
|
|
// Give it time to start
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
mgr.CancelTask("task-cancel-test12")
|
|
mgr.Wait()
|
|
}
|
|
|
|
func TestManagerPauseTask(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
|
|
dl := &slowMockDownloader{method: MethodTorrent}
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 2,
|
|
OutputDir: t.TempDir(),
|
|
}, reporter, dl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
go reporter.Run(ctx)
|
|
|
|
mgr.Submit(ctx, agent.Task{
|
|
ID: "task-pause-test123",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Pause Me",
|
|
PreferredMethod: "torrent",
|
|
})
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
mgr.PauseTask("task-pause-test123")
|
|
mgr.Wait()
|
|
}
|
|
|
|
func TestManagerCancelAndDeleteFiles(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
|
|
dl := &slowMockDownloader{method: MethodTorrent}
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 2,
|
|
OutputDir: t.TempDir(),
|
|
}, reporter, dl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
go reporter.Run(ctx)
|
|
|
|
mgr.Submit(ctx, agent.Task{
|
|
ID: "task-delfile-test12",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Delete Me",
|
|
PreferredMethod: "torrent",
|
|
})
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
mgr.CancelAndDeleteFiles("task-delfile-test12")
|
|
mgr.Wait()
|
|
}
|
|
|
|
func TestManagerCancelNonexistent(t *testing.T) {
|
|
reporter := NewProgressReporter(
|
|
agent.NewClient("http://localhost", "test", "test"),
|
|
1*time.Second,
|
|
)
|
|
mgr := NewManager(ManagerConfig{MaxConcurrent: 2}, reporter)
|
|
// Should not panic
|
|
mgr.CancelTask("nonexistent")
|
|
mgr.PauseTask("nonexistent")
|
|
mgr.CancelAndDeleteFiles("nonexistent")
|
|
}
|
|
|
|
// resultMockDownloader returns a configurable result
|
|
type resultMockDownloader struct {
|
|
method DownloadMethod
|
|
result *Result
|
|
}
|
|
|
|
func (m *resultMockDownloader) Method() DownloadMethod { return m.method }
|
|
func (m *resultMockDownloader) Available(_ context.Context, _ *Task) (bool, error) {
|
|
return true, nil
|
|
}
|
|
func (m *resultMockDownloader) Download(_ context.Context, _ *Task, _ string, _ chan<- Progress) (*Result, error) {
|
|
return m.result, nil
|
|
}
|
|
func (m *resultMockDownloader) Pause(_ string) error { return nil }
|
|
func (m *resultMockDownloader) Cancel(_ string) error { return nil }
|
|
func (m *resultMockDownloader) Shutdown(_ context.Context) error { return nil }
|
|
|
|
// slowMockDownloader blocks until context is cancelled
|
|
type slowMockDownloader struct {
|
|
method DownloadMethod
|
|
}
|
|
|
|
func (m *slowMockDownloader) Method() DownloadMethod { return m.method }
|
|
func (m *slowMockDownloader) Available(_ context.Context, _ *Task) (bool, error) {
|
|
return true, nil
|
|
}
|
|
func (m *slowMockDownloader) Download(ctx context.Context, _ *Task, _ string, _ chan<- Progress) (*Result, error) {
|
|
<-ctx.Done()
|
|
return nil, ctx.Err()
|
|
}
|
|
func (m *slowMockDownloader) Pause(_ string) error { return nil }
|
|
func (m *slowMockDownloader) Cancel(_ string) error { return nil }
|
|
func (m *slowMockDownloader) Shutdown(_ context.Context) error { return nil }
|