package download import ( "context" "testing" "time" ) // fakeDownload is a Download that blocks in Run until released, and (optionally) // announces a download->seed transition so the engine can free its -j slot. type fakeDownload struct { name string started chan struct{} // closed when Run begins seeding chan struct{} // SeedingStarted signal release chan struct{} // Run returns once this is closed detach bool // if set, Run signals seeding right after starting } func newFake(name string, detach bool) *fakeDownload { return &fakeDownload{ name: name, started: make(chan struct{}), seeding: make(chan struct{}), release: make(chan struct{}), detach: detach, } } func (f *fakeDownload) Name() string { return f.name } func (f *fakeDownload) Stat() Stat { return Stat{Name: f.name} } func (f *fakeDownload) SeedingStarted() <-chan struct{} { return f.seeding } func (f *fakeDownload) Run(ctx context.Context) error { close(f.started) if f.detach { close(f.seeding) } select { case <-f.release: return nil case <-ctx.Done(): return ctx.Err() } } func started(f *fakeDownload, d time.Duration) bool { select { case <-f.started: return true case <-time.After(d): return false } } // A torrent that has finished downloading and is now seeding must not keep // occupying a -j slot: with -j 1, a seeding download has to let a queued one run. func TestEngineSeedingFreesSlot(t *testing.T) { eng := NewEngine(1) a := newFake("a", true) b := newFake("b", true) ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan []Result, 1) go func() { done <- eng.Run(ctx, []Download{a, b}) }() // Neither is ever released, yet both must start: each frees the single slot // the instant it detaches into seeding. if !started(a, time.Second) || !started(b, time.Second) { t.Fatal("both downloads should start once seeding frees the -j slot") } close(a.release) close(b.release) <-done } // Control: without a seeding transition, -j 1 still serializes — the second // download waits for the first to finish before it starts. func TestEngineLimitsConcurrentDownloads(t *testing.T) { eng := NewEngine(1) a := newFake("a", false) b := newFake("b", false) ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan []Result, 1) go func() { done <- eng.Run(ctx, []Download{a, b}) }() var first, second *fakeDownload select { case <-a.started: first, second = a, b case <-b.started: first, second = b, a case <-time.After(time.Second): t.Fatal("one download should start") } if started(second, 100*time.Millisecond) { t.Fatal("second download must not start while the only slot is held") } close(first.release) // first finishes, freeing the slot if !started(second, time.Second) { t.Fatal("second download should start after the first frees the slot") } close(second.release) <-done }