Checked against slskd 0.26.0's source rather than a live daemon, which #267 never had. slskd reads a search's searchTimeout in seconds, counted from the last response. We sent milliseconds, telling it a search may idle for five hours, so a search never completed on its own. It now sends seconds, with slskd's floor of 5. slskd writes a finished file to <downloads>/<remote leaf folder>/, and when a name is taken it writes name_<ticks>.ext beside it. collect found files by name there, so a file left by an earlier failed attempt, or by the user's own download, was collected in place of this grab's. A user who changed slskd's destination setting got nothing collected at all. slskd 0.26 takes a batch download with an explicit destination. Each grab now enqueues batches into yellowjacket/<uuid>/ (one per disc, since a batch's files land flat), collects from exactly there, and removes the folder afterwards, including after a failure. An older daemon answers the batch route with 400, which is remembered, and the per-user enqueue is used. There, collect skips files that were already present, unchanged, before the enqueue, and takes the renamed copy slskd wrote instead. The comparison is against a snapshot, not a clock, because slskd may run on another machine. The per-folder lock from #272 is kept only while batches are not known to work. The test stub now writes files when they are enqueued, as slskd does, including the rename, so tests no longer stage files before a grab. An opt-in TestSlskdLive runs against a real daemon when YJ_SLSKD_URL, YJ_SLSKD_API_KEY and YJ_SLSKD_DOWNLOADS are set. Closes #274 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJiuc3ZZhxsPXz3ozTirT
131 lines
3.2 KiB
Go
131 lines
3.2 KiB
Go
package download
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestSlskdLive runs the provider against a real slskd daemon. It is
|
|
// skipped unless YJ_SLSKD_URL, YJ_SLSKD_API_KEY and YJ_SLSKD_DOWNLOADS
|
|
// are set, and it downloads something only when YJ_SLSKD_GRAB=1 — then
|
|
// the smallest candidate the search returns, from whichever stranger
|
|
// is sharing it.
|
|
//
|
|
// Everything else here tests the provider against a stub written from
|
|
// reading slskd's source. This is where those readings are checked:
|
|
// the search options, the responses endpoint, the batch destination,
|
|
// the cancel.
|
|
//
|
|
// YJ_SLSKD_URL=http://localhost:5030 YJ_SLSKD_API_KEY=… \
|
|
// YJ_SLSKD_DOWNLOADS=/path/to/slskd/downloads YJ_SLSKD_GRAB=1 \
|
|
// go test -run TestSlskdLive -v ./backend/download/
|
|
func TestSlskdLive(t *testing.T) {
|
|
base, key, downloads := os.Getenv("YJ_SLSKD_URL"),
|
|
os.Getenv("YJ_SLSKD_API_KEY"), os.Getenv("YJ_SLSKD_DOWNLOADS")
|
|
if base == "" || key == "" || downloads == "" {
|
|
t.Skip(
|
|
"set YJ_SLSKD_URL, YJ_SLSKD_API_KEY and YJ_SLSKD_DOWNLOADS to run against a real slskd",
|
|
)
|
|
}
|
|
|
|
query := os.Getenv("YJ_SLSKD_QUERY")
|
|
if query == "" {
|
|
query = "Radiohead OK Computer"
|
|
}
|
|
|
|
p, err := newSlskd(
|
|
Config{
|
|
ID: 1, Kind: KindSlskd, Name: "live", Enabled: true,
|
|
Settings: map[string]string{"url": base, "downloadsPath": downloads},
|
|
},
|
|
func(string) (string, error) { return key, nil },
|
|
slogDiscard(),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("newSlskd: %v", err)
|
|
}
|
|
|
|
s, ok := p.(*slskd)
|
|
if !ok {
|
|
t.Fatalf("provider is %T", p)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
if err := s.Check(ctx); err != nil {
|
|
t.Fatalf("Check: %v", err)
|
|
}
|
|
|
|
started := time.Now()
|
|
|
|
got, err := s.Search(ctx, Download{Query: query})
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
|
|
t.Logf(
|
|
"search %q: %d candidates in %s",
|
|
query,
|
|
len(got),
|
|
time.Since(started).Round(time.Millisecond),
|
|
)
|
|
|
|
if len(got) == 0 {
|
|
t.Fatal("no candidates; try a more common YJ_SLSKD_QUERY")
|
|
}
|
|
|
|
timed := 0
|
|
|
|
for _, c := range got {
|
|
for _, f := range c.Files {
|
|
if f.LengthMillis > 0 {
|
|
timed++
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Logf("%d files carry a length", timed)
|
|
|
|
if os.Getenv("YJ_SLSKD_GRAB") != "1" {
|
|
return
|
|
}
|
|
|
|
smallest := slices.MinFunc(got, func(a, b Candidate) int {
|
|
return int(a.TotalSize - b.TotalSize)
|
|
})
|
|
|
|
t.Logf("grabbing %q from %s (%d files, %d bytes)",
|
|
smallest.Title, smallest.Origin, len(smallest.Files), smallest.TotalSize)
|
|
|
|
s.stallAfter = 3 * time.Minute
|
|
|
|
gctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
|
|
defer cancel()
|
|
|
|
res, err := s.Grab(gctx, smallest, t.TempDir(), func(p Progress) {
|
|
t.Logf("%s: %d/%d bytes", p.Phase, p.Current, p.Total)
|
|
})
|
|
if err != nil {
|
|
// A stranger going offline is not a defect; what matters is
|
|
// that the transfers were cancelled, which slskd's UI shows.
|
|
t.Fatalf("Grab: %v", err)
|
|
}
|
|
|
|
t.Logf("batches: %v", s.batches.Load() == batchesSupported)
|
|
|
|
for _, f := range res.Files {
|
|
info, err := os.Stat(f)
|
|
if err != nil || info.Size() == 0 {
|
|
t.Errorf("collected %s is missing or empty: %v", f, err)
|
|
}
|
|
}
|
|
|
|
if entries, _ := os.ReadDir(filepath.Join(downloads, slskdDestRoot)); len(entries) != 0 {
|
|
t.Errorf("%d batch folders left in slskd's downloads", len(entries))
|
|
}
|
|
}
|