SeedRatio/SeedTime were declared on TorrentConfig but never consumed, and SeedEnabled was hardcoded false in both constructors — the daemon never seeded, and if forced it seeded forever. - config: [downloads] seed_enabled/seed_ratio/seed_time (opt-in, off by default) - daemon: parse seed_time + wire all three; startup log per target shape - engine: seedTargetReached() (pure) + seedAndDrop() background monitor on a downloader-scoped seedCtx (not the task ctx, which dies when Download returns); drops the torrent on ratio (uploaded/size) OR time, whichever first; no target = seed until shutdown. Configurable check interval (tests lower it). - fix: cleanup() now always drops — previously leaked the handle on error paths when seeding was enabled. - refactor: dropTracked() helper shared by cleanup + post-seeding drop. Tests: TestSeedTargetReached (9 cases) + ctx/no-target branches + loopback swarm smoke (-tags smoke). Roadmap hueco closed.
118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
//go:build smoke
|
|
|
|
package engine
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/anacrolix/torrent"
|
|
"github.com/anacrolix/torrent/bencode"
|
|
"github.com/anacrolix/torrent/metainfo"
|
|
)
|
|
|
|
// TestSeedLifecycleSmoke spins up a real loopback BitTorrent swarm: a seeder
|
|
// client serving a small file, and our TorrentDownloader's client leeching it.
|
|
// Once the leecher completes, the torrent is handed to seedAndDrop with a short
|
|
// SeedTime; the test asserts the lifecycle fires and the handle is dropped
|
|
// (removed from d.active). Exercises the real anacrolix Stats/Drop/ticker path,
|
|
// not mocks. Run with: go test -tags smoke -run TestSeedLifecycleSmoke ./internal/engine/
|
|
func TestSeedLifecycleSmoke(t *testing.T) {
|
|
// --- seeder: a real client serving a 4 MiB file over loopback ---
|
|
seedDir := t.TempDir()
|
|
payload := make([]byte, 4<<20)
|
|
for i := range payload {
|
|
payload[i] = byte(i)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(seedDir, "movie.bin"), payload, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var info metainfo.Info
|
|
info.PieceLength = 256 << 10
|
|
if err := info.BuildFromFilePath(filepath.Join(seedDir, "movie.bin")); err != nil {
|
|
t.Fatalf("build info: %v", err)
|
|
}
|
|
var mi metainfo.MetaInfo
|
|
var err error
|
|
if mi.InfoBytes, err = bencode.Marshal(info); err != nil {
|
|
t.Fatalf("marshal info: %v", err)
|
|
}
|
|
|
|
scfg := torrent.NewDefaultClientConfig()
|
|
scfg.DataDir = seedDir
|
|
scfg.Seed = true
|
|
scfg.NoDHT = true
|
|
scfg.DisableTrackers = true
|
|
scfg.ListenPort = 0 // random — never collides with the leecher's 42069
|
|
seeder, err := torrent.NewClient(scfg)
|
|
if err != nil {
|
|
t.Fatalf("seeder client: %v", err)
|
|
}
|
|
defer seeder.Close()
|
|
st, err := seeder.AddTorrent(&mi)
|
|
if err != nil {
|
|
t.Fatalf("seeder add: %v", err)
|
|
}
|
|
<-st.GotInfo()
|
|
st.DownloadAll() // verifies the existing pieces so the seeder is "complete"
|
|
|
|
// --- leecher: our downloader, seeding enabled, very short seed time ---
|
|
leechDir := t.TempDir()
|
|
dl, err := NewTorrentDownloader(TorrentConfig{
|
|
DataDir: leechDir,
|
|
SeedEnabled: true,
|
|
SeedTime: 1 * time.Second, // time target fires fast (no peers pull from us, so ratio stays 0)
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("downloader: %v", err)
|
|
}
|
|
dl.seedCheckInterval = 200 * time.Millisecond // poll fast so the 1s target is noticed promptly
|
|
defer dl.Shutdown(context.Background())
|
|
|
|
lt, err := dl.client.AddTorrent(&mi)
|
|
if err != nil {
|
|
t.Fatalf("leecher add: %v", err)
|
|
}
|
|
<-lt.GotInfo()
|
|
lt.AddClientPeer(seeder) // loopback peer — no DHT/tracker needed
|
|
lt.DownloadAll()
|
|
|
|
deadline := time.After(30 * time.Second)
|
|
for lt.BytesMissing() > 0 {
|
|
select {
|
|
case <-deadline:
|
|
t.Fatalf("download did not complete (missing %d bytes)", lt.BytesMissing())
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
t.Logf("leecher completed %d bytes", lt.BytesCompleted())
|
|
|
|
// Track it as the daemon would for a seeding torrent, then run the lifecycle.
|
|
const taskID = "smoke-seed-task-0001"
|
|
dl.activeMu.Lock()
|
|
dl.active[taskID] = lt
|
|
dl.activeMu.Unlock()
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
dl.seedAndDrop(taskID, lt, info.Length)
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("seedAndDrop did not return within 10s")
|
|
}
|
|
|
|
dl.activeMu.Lock()
|
|
_, stillTracked := dl.active[taskID]
|
|
dl.activeMu.Unlock()
|
|
if stillTracked {
|
|
t.Error("torrent still tracked after seedAndDrop — lifecycle did not drop it")
|
|
}
|
|
}
|