- 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
601 lines
17 KiB
Go
601 lines
17 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/torrentclaw/unarr/internal/agent"
|
|
)
|
|
|
|
// errorMockDownloader siempre falla en Download para simular fallo de método.
|
|
type errorMockDownloader struct {
|
|
method DownloadMethod
|
|
err error
|
|
}
|
|
|
|
func (m *errorMockDownloader) Method() DownloadMethod { return m.method }
|
|
func (m *errorMockDownloader) Available(_ context.Context, _ *Task) (bool, error) {
|
|
return true, nil
|
|
}
|
|
func (m *errorMockDownloader) Download(_ context.Context, _ *Task, _ string, _ chan<- Progress) (*Result, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
return nil, fmt.Errorf("simulated download failure for %s", m.method)
|
|
}
|
|
func (m *errorMockDownloader) Pause(_ string) error { return nil }
|
|
func (m *errorMockDownloader) Cancel(_ string) error { return nil }
|
|
func (m *errorMockDownloader) Shutdown(_ context.Context) error { return nil }
|
|
|
|
// makeProgressReporter crea un ProgressReporter con mock de reporter para tests de integración.
|
|
func makeProgressReporter() *ProgressReporter {
|
|
reporter := &mockStatusReporter{}
|
|
return &ProgressReporter{
|
|
reporter: reporter,
|
|
interval: 100 * time.Millisecond,
|
|
latest: make(map[string]*Task),
|
|
lastReported: make(map[string]TaskStatus),
|
|
}
|
|
}
|
|
|
|
// TestManagerPipeline_FullSuccess verifica el pipeline completo:
|
|
// submit → download → verify → complete con archivo real en disco.
|
|
func TestManagerPipeline_FullSuccess(t *testing.T) {
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "movie.mkv")
|
|
if err := os.WriteFile(filePath, make([]byte, 2048), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pr := makeProgressReporter()
|
|
dl := &resultMockDownloader{
|
|
method: MethodTorrent,
|
|
result: &Result{
|
|
FilePath: filePath,
|
|
FileName: "movie.mkv",
|
|
Method: MethodTorrent,
|
|
Size: 2048,
|
|
},
|
|
}
|
|
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 1,
|
|
OutputDir: dir,
|
|
}, pr, dl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
go pr.Run(ctx)
|
|
|
|
task := agent.Task{
|
|
ID: "integration-full-123456",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Test Movie",
|
|
PreferredMethod: "torrent",
|
|
}
|
|
mgr.Submit(ctx, task)
|
|
mgr.Wait()
|
|
}
|
|
|
|
// TestManagerPipeline_Fallback_TorrentFails_DebridSucceeds verifica que cuando
|
|
// torrent falla en modo "auto", el manager hace fallback a debrid.
|
|
func TestManagerPipeline_Fallback_TorrentFails_DebridSucceeds(t *testing.T) {
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "movie.mkv")
|
|
if err := os.WriteFile(filePath, make([]byte, 2048), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pr := makeProgressReporter()
|
|
|
|
// Torrent siempre falla
|
|
torrentDl := &errorMockDownloader{method: MethodTorrent}
|
|
// Debrid tiene éxito
|
|
debridDl := &resultMockDownloader{
|
|
method: MethodDebrid,
|
|
result: &Result{
|
|
FilePath: filePath,
|
|
FileName: "movie.mkv",
|
|
Method: MethodDebrid,
|
|
Size: 2048,
|
|
},
|
|
}
|
|
|
|
// Debrid debe declararse disponible — usamos mockDownloader para eso
|
|
debridAvailDl := struct {
|
|
*errorMockDownloader
|
|
*resultMockDownloader
|
|
}{torrentDl, debridDl}
|
|
_ = debridAvailDl // unused, kept for clarity
|
|
|
|
// Un mock que es available=true y retorna resultado exitoso
|
|
type debridFullMock struct {
|
|
resultMockDownloader
|
|
}
|
|
debridFull := &debridFullMock{
|
|
resultMockDownloader: resultMockDownloader{
|
|
method: MethodDebrid,
|
|
result: &Result{
|
|
FilePath: filePath,
|
|
FileName: "movie.mkv",
|
|
Method: MethodDebrid,
|
|
Size: 2048,
|
|
},
|
|
},
|
|
}
|
|
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 1,
|
|
OutputDir: dir,
|
|
}, pr, torrentDl, debridFull)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
go pr.Run(ctx)
|
|
|
|
// PreferredMethod: "auto" es necesario para que tryFallback funcione
|
|
task := agent.Task{
|
|
ID: "fallback-test-123456789",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Fallback Movie",
|
|
PreferredMethod: "auto",
|
|
}
|
|
mgr.Submit(ctx, task)
|
|
mgr.Wait()
|
|
// Si llegamos aquí sin timeout, el fallback funcionó (torrent falló, debrid tuvo éxito)
|
|
}
|
|
|
|
// TestManagerPipeline_AllMethodsFail verifica que cuando todos los downloaders
|
|
// fallan, la tarea termina en estado failed.
|
|
func TestManagerPipeline_AllMethodsFail(t *testing.T) {
|
|
dir := t.TempDir()
|
|
pr := makeProgressReporter()
|
|
|
|
torrentDl := &errorMockDownloader{method: MethodTorrent, err: fmt.Errorf("no peers")}
|
|
// En modo "torrent" específico no hay fallback
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 1,
|
|
OutputDir: dir,
|
|
}, pr, torrentDl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
go pr.Run(ctx)
|
|
|
|
task := agent.Task{
|
|
ID: "fail-all-123456789012",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Failing Download",
|
|
PreferredMethod: "torrent",
|
|
}
|
|
mgr.Submit(ctx, task)
|
|
mgr.Wait()
|
|
// Si llegamos aquí, el manager manejó el fallo sin panic ni deadlock
|
|
}
|
|
|
|
// TestManagerPipeline_MultiConcurrent verifica que múltiples descargas concurrentes
|
|
// completan todas correctamente.
|
|
func TestManagerPipeline_MultiConcurrent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
const numTasks = 3
|
|
|
|
// Crear archivos para cada tarea
|
|
files := make([]string, numTasks)
|
|
for i := 0; i < numTasks; i++ {
|
|
files[i] = filepath.Join(dir, fmt.Sprintf("movie%d.mkv", i))
|
|
if err := os.WriteFile(files[i], make([]byte, 1024), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
var submitCount atomic.Int32
|
|
pr := makeProgressReporter()
|
|
|
|
// Usar un mock que devuelve archivos distintos por tarea
|
|
dl := &multiResultMockDownloader{dir: dir, files: files}
|
|
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: numTasks,
|
|
OutputDir: dir,
|
|
}, pr, dl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
go pr.Run(ctx)
|
|
|
|
for i := 0; i < numTasks; i++ {
|
|
submitCount.Add(1)
|
|
task := agent.Task{
|
|
ID: fmt.Sprintf("concurrent-task-%02d-123456", i),
|
|
InfoHash: fmt.Sprintf("abc%037d", i), // 40 hex chars
|
|
Title: fmt.Sprintf("Movie %d", i),
|
|
PreferredMethod: "torrent",
|
|
}
|
|
mgr.Submit(ctx, task)
|
|
}
|
|
|
|
mgr.Wait()
|
|
|
|
if submitCount.Load() != int32(numTasks) {
|
|
t.Errorf("submitted %d tasks, want %d", submitCount.Load(), numTasks)
|
|
}
|
|
}
|
|
|
|
// multiResultMockDownloader devuelve archivos distintos según el orden de llamadas.
|
|
type multiResultMockDownloader struct {
|
|
dir string
|
|
files []string
|
|
callCount atomic.Int32
|
|
}
|
|
|
|
func (m *multiResultMockDownloader) Method() DownloadMethod { return MethodTorrent }
|
|
func (m *multiResultMockDownloader) Available(_ context.Context, _ *Task) (bool, error) {
|
|
return true, nil
|
|
}
|
|
func (m *multiResultMockDownloader) Download(_ context.Context, _ *Task, _ string, _ chan<- Progress) (*Result, error) {
|
|
idx := int(m.callCount.Add(1)) - 1
|
|
if idx >= len(m.files) {
|
|
return nil, fmt.Errorf("too many calls to multiResultMockDownloader")
|
|
}
|
|
return &Result{
|
|
FilePath: m.files[idx],
|
|
FileName: filepath.Base(m.files[idx]),
|
|
Method: MethodTorrent,
|
|
Size: 1024,
|
|
}, nil
|
|
}
|
|
func (m *multiResultMockDownloader) Pause(_ string) error { return nil }
|
|
func (m *multiResultMockDownloader) Cancel(_ string) error { return nil }
|
|
func (m *multiResultMockDownloader) Shutdown(_ context.Context) error { return nil }
|
|
|
|
// TestManagerPipeline_CancelTaskMidDownload verifica que CancelTask() durante una
|
|
// descarga activa libera el slot y no produce deadlock.
|
|
func TestManagerPipeline_CancelTaskMidDownload(t *testing.T) {
|
|
dir := t.TempDir()
|
|
pr := makeProgressReporter()
|
|
dl := &slowMockDownloader{method: MethodTorrent}
|
|
|
|
const taskID = "cancel-mid-test-12345"
|
|
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 2,
|
|
OutputDir: dir,
|
|
}, pr, dl)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go pr.Run(ctx)
|
|
|
|
task := agent.Task{
|
|
ID: taskID,
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Cancel Test",
|
|
PreferredMethod: "torrent",
|
|
}
|
|
mgr.Submit(ctx, task)
|
|
|
|
// Esperar a que la tarea esté activa
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Cancelar la tarea específica (cancela su contexto interno)
|
|
mgr.CancelTask(taskID)
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
mgr.Wait()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// OK — manager terminó limpiamente tras CancelTask
|
|
case <-time.After(5 * time.Second):
|
|
t.Error("Manager.Wait() timed out after CancelTask — possible deadlock")
|
|
}
|
|
}
|
|
|
|
// TestManagerPipeline_OnTaskDone_Called verifica que el callback OnTaskDone
|
|
// se llama exactamente una vez cuando una tarea completa.
|
|
func TestManagerPipeline_OnTaskDone_Called(t *testing.T) {
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "movie.mkv")
|
|
if err := os.WriteFile(filePath, make([]byte, 1024), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pr := makeProgressReporter()
|
|
dl := &resultMockDownloader{
|
|
method: MethodTorrent,
|
|
result: &Result{FilePath: filePath, FileName: "movie.mkv", Method: MethodTorrent, Size: 1024},
|
|
}
|
|
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 1,
|
|
OutputDir: dir,
|
|
}, pr, dl)
|
|
|
|
var callCount atomic.Int32
|
|
mgr.OnTaskDone = func() {
|
|
callCount.Add(1)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
go pr.Run(ctx)
|
|
|
|
task := agent.Task{
|
|
ID: "ontaskdone-test-123456",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Done Callback Test",
|
|
PreferredMethod: "torrent",
|
|
}
|
|
mgr.Submit(ctx, task)
|
|
mgr.Wait()
|
|
|
|
if callCount.Load() != 1 {
|
|
t.Errorf("OnTaskDone called %d times, want 1", callCount.Load())
|
|
}
|
|
}
|
|
|
|
// TestManagerPipeline_RecentFinished_DrainedOnSync verifica que TaskStates()
|
|
// incluye tareas recientemente finalizadas y las limpia en la siguiente llamada.
|
|
func TestManagerPipeline_RecentFinished_DrainedOnSync(t *testing.T) {
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "movie.mkv")
|
|
if err := os.WriteFile(filePath, make([]byte, 1024), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pr := makeProgressReporter()
|
|
dl := &resultMockDownloader{
|
|
method: MethodTorrent,
|
|
result: &Result{FilePath: filePath, FileName: "movie.mkv", Method: MethodTorrent, Size: 1024},
|
|
}
|
|
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 1,
|
|
OutputDir: dir,
|
|
}, pr, dl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
go pr.Run(ctx)
|
|
|
|
task := agent.Task{
|
|
ID: "recent-finished-12345",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Recent Test",
|
|
PreferredMethod: "torrent",
|
|
}
|
|
mgr.Submit(ctx, task)
|
|
mgr.Wait()
|
|
|
|
// Primera llamada a TaskStates() debe incluir la tarea finalizada
|
|
states := mgr.TaskStates()
|
|
|
|
// La tarea se eliminó del mapa active, pero debe estar en recentFinished
|
|
foundRecent := false
|
|
for _, s := range states {
|
|
if s.TaskID == task.ID {
|
|
foundRecent = true
|
|
break
|
|
}
|
|
}
|
|
if !foundRecent {
|
|
t.Error("TaskStates() should include recently finished task in first call")
|
|
}
|
|
|
|
// Segunda llamada: recentFinished debe estar vacío (ya se drenó)
|
|
states2 := mgr.TaskStates()
|
|
for _, s := range states2 {
|
|
if s.TaskID == task.ID {
|
|
t.Error("TaskStates() should NOT include finished task in second call (should be drained)")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestManagerPipeline_ForceStart_BypassesSemaphore verifica que ForceStart=true
|
|
// permite iniciar descargas aunque el semáforo esté lleno.
|
|
func TestManagerPipeline_ForceStart_BypassesSemaphore(t *testing.T) {
|
|
dir := t.TempDir()
|
|
pr := makeProgressReporter()
|
|
|
|
// slowMock bloqueará el semáforo
|
|
slowDl := &slowMockDownloader{method: MethodTorrent}
|
|
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 1, // semáforo de 1
|
|
OutputDir: dir,
|
|
}, pr, slowDl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
go pr.Run(ctx)
|
|
|
|
// Primera tarea: llena el semáforo
|
|
task1 := agent.Task{
|
|
ID: "force-start-slow-12345",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Slow Task",
|
|
PreferredMethod: "torrent",
|
|
}
|
|
mgr.Submit(ctx, task1)
|
|
|
|
// Pequeña pausa para que task1 adquiera el semáforo
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Segunda tarea con ForceStart=true: debe empezar aunque semáforo lleno
|
|
filePath := filepath.Join(dir, "force.mkv")
|
|
if err := os.WriteFile(filePath, make([]byte, 512), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Para ForceStart necesitamos un downloader que tenga éxito inmediato
|
|
// Usar resultMockDownloader pero ForceStart necesita el mismo downloader registrado
|
|
// Modificamos el test: verificar que ActiveCount() > MaxConcurrent con ForceStart
|
|
task2 := agent.Task{
|
|
ID: "force-start-fast-12345",
|
|
InfoHash: "def456abc123def456abc123def456abc123def4",
|
|
Title: "Force Task",
|
|
PreferredMethod: "torrent",
|
|
ForceStart: true,
|
|
}
|
|
mgr.Submit(ctx, task2)
|
|
|
|
// Verificar que hay más tareas activas que el límite del semáforo
|
|
time.Sleep(50 * time.Millisecond)
|
|
active := mgr.ActiveCount()
|
|
if active < 1 {
|
|
t.Errorf("expected at least 1 active task with ForceStart, got %d", active)
|
|
}
|
|
|
|
cancel() // terminar las tareas lentas
|
|
mgr.Wait()
|
|
}
|
|
|
|
// TestManagerPipeline_Organize_MoviesDir verifica que cuando organize está
|
|
// habilitado y ContentType es "movie", el archivo se mueve al directorio correcto.
|
|
func TestManagerPipeline_Organize_MoviesDir(t *testing.T) {
|
|
downloadDir := t.TempDir()
|
|
moviesDir := t.TempDir()
|
|
|
|
filePath := filepath.Join(downloadDir, "movie.mkv")
|
|
if err := os.WriteFile(filePath, make([]byte, 1024), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pr := makeProgressReporter()
|
|
dl := &resultMockDownloader{
|
|
method: MethodTorrent,
|
|
result: &Result{
|
|
FilePath: filePath,
|
|
FileName: "movie.mkv",
|
|
Method: MethodTorrent,
|
|
Size: 1024,
|
|
},
|
|
}
|
|
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 1,
|
|
OutputDir: downloadDir,
|
|
Organize: OrganizeConfig{
|
|
Enabled: true,
|
|
MoviesDir: moviesDir,
|
|
OutputDir: downloadDir,
|
|
},
|
|
}, pr, dl)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
go pr.Run(ctx)
|
|
|
|
task := agent.Task{
|
|
ID: "organize-test-1234567",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "The Matrix 1999",
|
|
PreferredMethod: "torrent",
|
|
}
|
|
mgr.Submit(ctx, task)
|
|
mgr.Wait()
|
|
|
|
// El archivo debe haberse movido a moviesDir (o seguir en downloadDir si hay error de organización)
|
|
// Lo que nos importa es que no haya crash
|
|
}
|
|
|
|
// TestManagerPipeline_Shutdown_GracefulWithActiveDownloads verifica que Shutdown()
|
|
// espera a que terminen las descargas activas antes de salir.
|
|
func TestManagerPipeline_Shutdown_GracefulWithActiveDownloads(t *testing.T) {
|
|
dir := t.TempDir()
|
|
pr := makeProgressReporter()
|
|
|
|
// Downloader que tarda un poco pero termina
|
|
dl := &timedResultMockDownloader{
|
|
method: MethodTorrent,
|
|
delay: 100 * time.Millisecond,
|
|
dir: dir,
|
|
content: make([]byte, 512),
|
|
}
|
|
|
|
mgr := NewManager(ManagerConfig{
|
|
MaxConcurrent: 2,
|
|
OutputDir: dir,
|
|
}, pr, dl)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go pr.Run(ctx)
|
|
|
|
task := agent.Task{
|
|
ID: "shutdown-graceful-123",
|
|
InfoHash: "abc123def456abc123def456abc123def456abc1",
|
|
Title: "Graceful Test",
|
|
PreferredMethod: "torrent",
|
|
}
|
|
mgr.Submit(ctx, task)
|
|
|
|
// Dar tiempo a que la tarea empiece
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
// Shutdown con timeout suficiente para que la tarea termine
|
|
shutCtx, shutCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer shutCancel()
|
|
|
|
start := time.Now()
|
|
mgr.Shutdown(shutCtx)
|
|
elapsed := time.Since(start)
|
|
|
|
if elapsed > 4*time.Second {
|
|
t.Errorf("Shutdown took too long: %v", elapsed)
|
|
}
|
|
}
|
|
|
|
// timedResultMockDownloader simula una descarga que tarda un tiempo específico.
|
|
type timedResultMockDownloader struct {
|
|
method DownloadMethod
|
|
delay time.Duration
|
|
dir string
|
|
content []byte
|
|
}
|
|
|
|
func (m *timedResultMockDownloader) Method() DownloadMethod { return m.method }
|
|
func (m *timedResultMockDownloader) Available(_ context.Context, _ *Task) (bool, error) {
|
|
return true, nil
|
|
}
|
|
func (m *timedResultMockDownloader) Download(ctx context.Context, task *Task, outputDir string, _ chan<- Progress) (*Result, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-time.After(m.delay):
|
|
}
|
|
|
|
filePath := filepath.Join(outputDir, "timed.mkv")
|
|
if err := os.WriteFile(filePath, m.content, 0o644); err != nil {
|
|
return nil, err
|
|
}
|
|
return &Result{
|
|
FilePath: filePath,
|
|
FileName: "timed.mkv",
|
|
Method: m.method,
|
|
Size: int64(len(m.content)),
|
|
}, nil
|
|
}
|
|
func (m *timedResultMockDownloader) Pause(_ string) error { return nil }
|
|
func (m *timedResultMockDownloader) Cancel(_ string) error { return nil }
|
|
func (m *timedResultMockDownloader) Shutdown(_ context.Context) error { return nil }
|
|
|
|
// TestManagerPipeline_FreeSlots verifica que FreeSlots() refleja el número
|
|
// correcto de slots disponibles.
|
|
func TestManagerPipeline_FreeSlots(t *testing.T) {
|
|
pr := makeProgressReporter()
|
|
mgr := NewManager(ManagerConfig{MaxConcurrent: 3}, pr)
|
|
|
|
if slots := mgr.FreeSlots(); slots != 3 {
|
|
t.Errorf("FreeSlots() = %d, want 3 when empty", slots)
|
|
}
|
|
}
|