- Refactor download.go and stream.go with downloadDeps/streamDeps structs for dependency injection, enabling unit testing without real I/O - download_test.go: 15 tests — input validation, mock downloaders, method selection, cobra Args, deadlock detection - stream_test.go: input validation, noOpen flag, engine error handling - client_test.go: context cancellation, timeout, full Sync roundtrip, watch-progress and HTTP error unwrapping - sync_test.go: TriggerSync on watching transition, adjustInterval - torrent_test.go: TorrentDownloader lifecycle without network - stream_server_test.go: HTTP server lifecycle, SetFile/ClearFile, concurrent requests, Shutdown releases port, content-type - manager_integration_test.go: full pipeline — success, torrent→debrid fallback, all-fail, multi-concurrent, ForceStart, OnTaskDone, recent-finished drain, cancel mid-download, organize - usenet_test.go: Cancel/Pause race regression test (run with -race) - daemon_test.go: isAllowedStreamPath table tests - CI: split coverage gate to engine+agent only (50% threshold); cmd coverage still reported but not gated (interactive UI commands) - lefthook: add pre-push hook with go test -race -count=1 -timeout=120s
76 lines
2.3 KiB
Go
76 lines
2.3 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/torrentclaw/unarr/internal/agent"
|
|
"github.com/torrentclaw/unarr/internal/usenet/download"
|
|
"github.com/torrentclaw/unarr/internal/usenet/nzb"
|
|
)
|
|
|
|
// emptyNZB returns a minimal NZB with no files, suitable for test tracker creation.
|
|
func emptyNZB() *nzb.NZB { return &nzb.NZB{} }
|
|
|
|
// TestUsenetDownloader_Cancel_NoRace verifies that Cancel() reads tracker and taskDir
|
|
// under the mutex, avoiding a data race with Download() which writes them under the same lock.
|
|
// Run with -race to detect the race if it regresses.
|
|
func TestUsenetDownloader_Cancel_NoRace(t *testing.T) {
|
|
u := NewUsenetDownloader(agent.NewClient("http://localhost", "", "test"))
|
|
|
|
const taskID = "race-test-taskid-123456"
|
|
|
|
// Inject a fake activeDownload without tracker/taskDir set yet.
|
|
// We only need the cancel func; discard the context itself.
|
|
_, cancel := context.WithCancel(context.Background())
|
|
dl := &activeDownload{cancel: cancel}
|
|
u.mu.Lock()
|
|
u.active[taskID] = dl
|
|
u.mu.Unlock()
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Goroutine 1: simulates Download() setting tracker and taskDir under lock.
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 50; i++ {
|
|
tracker := download.NewProgressTracker(taskID, emptyNZB(), t.TempDir())
|
|
u.mu.Lock()
|
|
dl.tracker = tracker
|
|
dl.taskDir = t.TempDir()
|
|
u.mu.Unlock()
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}()
|
|
|
|
// Goroutine 2: calls Cancel() concurrently — must read under lock.
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 50; i++ {
|
|
u.Cancel(taskID) //nolint:errcheck
|
|
time.Sleep(time.Microsecond)
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestUsenetDownloader_Cancel_NonExistent verifies Cancel on unknown task returns nil.
|
|
func TestUsenetDownloader_Cancel_NonExistent(t *testing.T) {
|
|
u := NewUsenetDownloader(agent.NewClient("http://localhost", "", "test"))
|
|
if err := u.Cancel("no-such-task"); err != nil {
|
|
t.Errorf("Cancel non-existent task = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
// TestUsenetDownloader_Pause_NonExistent verifies Pause on unknown task returns nil.
|
|
func TestUsenetDownloader_Pause_NonExistent(t *testing.T) {
|
|
u := NewUsenetDownloader(agent.NewClient("http://localhost", "", "test"))
|
|
if err := u.Pause("no-such-task"); err != nil {
|
|
t.Errorf("Pause non-existent task = %v, want nil", err)
|
|
}
|
|
}
|