108 lines
2.9 KiB
Go
108 lines
2.9 KiB
Go
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
|
|
}
|