]> Sergey Matveev's repositories - btrtrc.git/blobdiff - client_test.go
Drop support for go 1.20
[btrtrc.git] / client_test.go
index a4520baee1dfbbc27b79bb6bb2e23d3f7d264408..d2a88e9e7a769abcd1871092c006e37b33573a51 100644 (file)
@@ -4,24 +4,23 @@ import (
        "encoding/binary"
        "fmt"
        "io"
-       "io/ioutil"
-       "log"
+       "net"
+       "net/netip"
        "os"
        "path/filepath"
        "reflect"
-       "sync"
        "testing"
+       "testing/iotest"
        "time"
 
-       "github.com/bradfitz/iter"
+       "github.com/anacrolix/dht/v2"
+       "github.com/anacrolix/log"
+       "github.com/anacrolix/missinggo/v2"
+       "github.com/anacrolix/missinggo/v2/filecache"
+       "github.com/frankban/quicktest"
+       qt "github.com/frankban/quicktest"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
-       "golang.org/x/time/rate"
-
-       "github.com/anacrolix/dht/v2"
-       _ "github.com/anacrolix/envpprof"
-       "github.com/anacrolix/missinggo"
-       "github.com/anacrolix/missinggo/filecache"
 
        "github.com/anacrolix/torrent/bencode"
        "github.com/anacrolix/torrent/internal/testutil"
@@ -30,57 +29,24 @@ import (
        "github.com/anacrolix/torrent/storage"
 )
 
-func TestingConfig() *ClientConfig {
-       cfg := NewDefaultClientConfig()
-       cfg.ListenHost = LoopbackListenHost
-       cfg.NoDHT = true
-       cfg.DataDir = tempDir()
-       cfg.DisableTrackers = true
-       cfg.NoDefaultPortForwarding = true
-       cfg.DisableAcceptRateLimiting = true
-       cfg.ListenPort = 0
-       //cfg.Debug = true
-       //cfg.Logger = cfg.Logger.WithText(func(m log.Msg) string {
-       //      t := m.Text()
-       //      m.Values(func(i interface{}) bool {
-       //              t += fmt.Sprintf("\n%[1]T: %[1]v", i)
-       //              return true
-       //      })
-       //      return t
-       //})
-       return cfg
-}
-
 func TestClientDefault(t *testing.T) {
-       cl, err := NewClient(TestingConfig())
+       cl, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
-       cl.Close()
+       require.Empty(t, cl.Close())
 }
 
 func TestClientNilConfig(t *testing.T) {
+       // The default config will put crap in the working directory.
+       origDir, _ := os.Getwd()
+       defer os.Chdir(origDir)
+       os.Chdir(t.TempDir())
        cl, err := NewClient(nil)
        require.NoError(t, err)
-       cl.Close()
-}
-
-func TestBoltPieceCompletionClosedWhenClientClosed(t *testing.T) {
-       cfg := TestingConfig()
-       pc, err := storage.NewBoltPieceCompletion(cfg.DataDir)
-       require.NoError(t, err)
-       ci := storage.NewFileWithCompletion(cfg.DataDir, pc)
-       defer ci.Close()
-       cfg.DefaultStorage = ci
-       cl, err := NewClient(cfg)
-       require.NoError(t, err)
-       cl.Close()
-       // And again, https://github.com/anacrolix/torrent/issues/158
-       cl, err = NewClient(cfg)
-       require.NoError(t, err)
-       cl.Close()
+       require.Empty(t, cl.Close())
 }
 
 func TestAddDropTorrent(t *testing.T) {
-       cl, err := NewClient(TestingConfig())
+       cl, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
        defer cl.Close()
        dir, mi := testutil.GreetingTestTorrent()
@@ -115,17 +81,16 @@ func TestPieceHashSize(t *testing.T) {
 func TestTorrentInitialState(t *testing.T) {
        dir, mi := testutil.GreetingTestTorrent()
        defer os.RemoveAll(dir)
-       cl := &Client{
-               config: TestingConfig(),
-       }
+       var cl Client
+       cl.init(TestingConfig(t))
        cl.initLogger()
        tor := cl.newTorrent(
                mi.HashInfoBytes(),
-               storage.NewFileWithCompletion(tempDir(), storage.NewMapPieceCompletion()),
+               storage.NewFileWithCompletion(t.TempDir(), storage.NewMapPieceCompletion()),
        )
        tor.setChunkSize(2)
        tor.cl.lock()
-       err := tor.setInfoBytes(mi.InfoBytes)
+       err := tor.setInfoBytesLocked(mi.InfoBytes)
        tor.cl.unlock()
        require.NoError(t, err)
        require.Len(t, tor.pieces, 3)
@@ -133,7 +98,7 @@ func TestTorrentInitialState(t *testing.T) {
        tor.cl.lock()
        assert.EqualValues(t, 3, tor.pieceNumPendingChunks(0))
        tor.cl.unlock()
-       assert.EqualValues(t, chunkSpec{4, 1}, chunkIndexSpec(2, tor.pieceLength(0), tor.chunkSize))
+       assert.EqualValues(t, ChunkSpec{4, 1}, chunkIndexSpec(2, tor.pieceLength(0), tor.chunkSize))
 }
 
 func TestReducedDialTimeout(t *testing.T) {
@@ -163,10 +128,10 @@ func TestReducedDialTimeout(t *testing.T) {
 }
 
 func TestAddDropManyTorrents(t *testing.T) {
-       cl, err := NewClient(TestingConfig())
+       cl, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
        defer cl.Close()
-       for i := range iter.N(1000) {
+       for i := 0; i < 1000; i += 1 {
                var spec TorrentSpec
                binary.PutVarint(spec.InfoHash[:], int64(i))
                tt, new, err := cl.AddTorrentSpec(&spec)
@@ -176,319 +141,17 @@ func TestAddDropManyTorrents(t *testing.T) {
        }
 }
 
-type FileCacheClientStorageFactoryParams struct {
-       Capacity    int64
-       SetCapacity bool
-       Wrapper     func(*filecache.Cache) storage.ClientImpl
-}
-
-func NewFileCacheClientStorageFactory(ps FileCacheClientStorageFactoryParams) storageFactory {
-       return func(dataDir string) storage.ClientImpl {
-               fc, err := filecache.NewCache(dataDir)
-               if err != nil {
-                       panic(err)
-               }
-               if ps.SetCapacity {
-                       fc.SetCapacity(ps.Capacity)
-               }
-               return ps.Wrapper(fc)
-       }
-}
-
-type storageFactory func(string) storage.ClientImpl
-
-func TestClientTransferDefault(t *testing.T) {
-       testClientTransfer(t, testClientTransferParams{
-               ExportClientStatus: true,
-               LeecherStorage: NewFileCacheClientStorageFactory(FileCacheClientStorageFactoryParams{
-                       Wrapper: fileCachePieceResourceStorage,
-               }),
-       })
-}
-
-func TestClientTransferRateLimitedUpload(t *testing.T) {
-       started := time.Now()
-       testClientTransfer(t, testClientTransferParams{
-               // We are uploading 13 bytes (the length of the greeting torrent). The
-               // chunks are 2 bytes in length. Then the smallest burst we can run
-               // with is 2. Time taken is (13-burst)/rate.
-               SeederUploadRateLimiter: rate.NewLimiter(11, 2),
-               ExportClientStatus:      true,
-       })
-       require.True(t, time.Since(started) > time.Second)
-}
-
-func TestClientTransferRateLimitedDownload(t *testing.T) {
-       testClientTransfer(t, testClientTransferParams{
-               LeecherDownloadRateLimiter: rate.NewLimiter(512, 512),
-       })
-}
-
 func fileCachePieceResourceStorage(fc *filecache.Cache) storage.ClientImpl {
-       return storage.NewResourcePieces(fc.AsResourceProvider())
-}
-
-func testClientTransferSmallCache(t *testing.T, setReadahead bool, readahead int64) {
-       testClientTransfer(t, testClientTransferParams{
-               LeecherStorage: NewFileCacheClientStorageFactory(FileCacheClientStorageFactoryParams{
-                       SetCapacity: true,
-                       // Going below the piece length means it can't complete a piece so
-                       // that it can be hashed.
-                       Capacity: 5,
-                       Wrapper:  fileCachePieceResourceStorage,
-               }),
-               SetReadahead: setReadahead,
-               // Can't readahead too far or the cache will thrash and drop data we
-               // thought we had.
-               Readahead:          readahead,
-               ExportClientStatus: true,
-       })
-}
-
-func TestClientTransferSmallCachePieceSizedReadahead(t *testing.T) {
-       testClientTransferSmallCache(t, true, 5)
-}
-
-func TestClientTransferSmallCacheLargeReadahead(t *testing.T) {
-       testClientTransferSmallCache(t, true, 15)
-}
-
-func TestClientTransferSmallCacheDefaultReadahead(t *testing.T) {
-       testClientTransferSmallCache(t, false, -1)
-}
-
-func TestClientTransferVarious(t *testing.T) {
-       // Leecher storage
-       for _, ls := range []storageFactory{
-               NewFileCacheClientStorageFactory(FileCacheClientStorageFactoryParams{
-                       Wrapper: fileCachePieceResourceStorage,
-               }),
-               storage.NewBoltDB,
-       } {
-               // Seeder storage
-               for _, ss := range []func(string) storage.ClientImpl{
-                       storage.NewFile,
-                       storage.NewMMap,
-               } {
-                       for _, responsive := range []bool{false, true} {
-                               testClientTransfer(t, testClientTransferParams{
-                                       Responsive:     responsive,
-                                       SeederStorage:  ss,
-                                       LeecherStorage: ls,
-                               })
-                               for _, readahead := range []int64{-1, 0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15, 20} {
-                                       testClientTransfer(t, testClientTransferParams{
-                                               SeederStorage:  ss,
-                                               Responsive:     responsive,
-                                               SetReadahead:   true,
-                                               Readahead:      readahead,
-                                               LeecherStorage: ls,
-                                       })
-                               }
-                       }
-               }
-       }
-}
-
-type testClientTransferParams struct {
-       Responsive                 bool
-       Readahead                  int64
-       SetReadahead               bool
-       ExportClientStatus         bool
-       LeecherStorage             func(string) storage.ClientImpl
-       SeederStorage              func(string) storage.ClientImpl
-       SeederUploadRateLimiter    *rate.Limiter
-       LeecherDownloadRateLimiter *rate.Limiter
-}
-
-func logPieceStateChanges(t *Torrent) {
-       sub := t.SubscribePieceStateChanges()
-       go func() {
-               defer sub.Close()
-               for e := range sub.Values {
-                       log.Printf("%p %#v", t, e)
-               }
-       }()
-}
-
-// Creates a seeder and a leecher, and ensures the data transfers when a read
-// is attempted on the leecher.
-func testClientTransfer(t *testing.T, ps testClientTransferParams) {
-       greetingTempDir, mi := testutil.GreetingTestTorrent()
-       defer os.RemoveAll(greetingTempDir)
-       // Create seeder and a Torrent.
-       cfg := TestingConfig()
-       cfg.Seed = true
-       if ps.SeederUploadRateLimiter != nil {
-               cfg.UploadRateLimiter = ps.SeederUploadRateLimiter
-       }
-       // cfg.ListenAddr = "localhost:4000"
-       if ps.SeederStorage != nil {
-               cfg.DefaultStorage = ps.SeederStorage(greetingTempDir)
-               defer cfg.DefaultStorage.Close()
-       } else {
-               cfg.DataDir = greetingTempDir
-       }
-       seeder, err := NewClient(cfg)
-       require.NoError(t, err)
-       if ps.ExportClientStatus {
-               defer testutil.ExportStatusWriter(seeder, "s")()
-       }
-       seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
-       // Run a Stats right after Closing the Client. This will trigger the Stats
-       // panic in #214 caused by RemoteAddr on Closed uTP sockets.
-       defer seederTorrent.Stats()
-       defer seeder.Close()
-       seederTorrent.VerifyData()
-       // Create leecher and a Torrent.
-       leecherDataDir, err := ioutil.TempDir("", "")
-       require.NoError(t, err)
-       defer os.RemoveAll(leecherDataDir)
-       cfg = TestingConfig()
-       if ps.LeecherStorage == nil {
-               cfg.DataDir = leecherDataDir
-       } else {
-               cfg.DefaultStorage = ps.LeecherStorage(leecherDataDir)
-       }
-       if ps.LeecherDownloadRateLimiter != nil {
-               cfg.DownloadRateLimiter = ps.LeecherDownloadRateLimiter
-       }
-       cfg.Seed = false
-       //cfg.Debug = true
-       leecher, err := NewClient(cfg)
-       require.NoError(t, err)
-       defer leecher.Close()
-       if ps.ExportClientStatus {
-               defer testutil.ExportStatusWriter(leecher, "l")()
-       }
-       leecherTorrent, new, err := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
-               ret = TorrentSpecFromMetaInfo(mi)
-               ret.ChunkSize = 2
-               return
-       }())
-       require.NoError(t, err)
-       assert.True(t, new)
-
-       //// This was used when observing coalescing of piece state changes.
-       //logPieceStateChanges(leecherTorrent)
-
-       // Now do some things with leecher and seeder.
-       leecherTorrent.AddClientPeer(seeder)
-       // The Torrent should not be interested in obtaining peers, so the one we
-       // just added should be the only one.
-       assert.False(t, leecherTorrent.Seeding())
-       assert.EqualValues(t, 1, leecherTorrent.Stats().PendingPeers)
-       r := leecherTorrent.NewReader()
-       defer r.Close()
-       if ps.Responsive {
-               r.SetResponsive()
-       }
-       if ps.SetReadahead {
-               r.SetReadahead(ps.Readahead)
-       }
-       assertReadAllGreeting(t, r)
-
-       seederStats := seederTorrent.Stats()
-       assert.True(t, 13 <= seederStats.BytesWrittenData.Int64())
-       assert.True(t, 8 <= seederStats.ChunksWritten.Int64())
-
-       leecherStats := leecherTorrent.Stats()
-       assert.True(t, 13 <= leecherStats.BytesReadData.Int64())
-       assert.True(t, 8 <= leecherStats.ChunksRead.Int64())
-
-       // Try reading through again for the cases where the torrent data size
-       // exceeds the size of the cache.
-       assertReadAllGreeting(t, r)
-}
-
-func assertReadAllGreeting(t *testing.T, r io.ReadSeeker) {
-       pos, err := r.Seek(0, io.SeekStart)
-       assert.NoError(t, err)
-       assert.EqualValues(t, 0, pos)
-       _greeting, err := ioutil.ReadAll(r)
-       assert.NoError(t, err)
-       assert.EqualValues(t, testutil.GreetingFileContents, _greeting)
-}
-
-// Check that after completing leeching, a leecher transitions to a seeding
-// correctly. Connected in a chain like so: Seeder <-> Leecher <-> LeecherLeecher.
-func TestSeedAfterDownloading(t *testing.T) {
-       greetingTempDir, mi := testutil.GreetingTestTorrent()
-       defer os.RemoveAll(greetingTempDir)
-
-       cfg := TestingConfig()
-       cfg.Seed = true
-       cfg.DataDir = greetingTempDir
-       seeder, err := NewClient(cfg)
-       require.NoError(t, err)
-       defer seeder.Close()
-       defer testutil.ExportStatusWriter(seeder, "s")()
-       seederTorrent, ok, err := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
-       require.NoError(t, err)
-       assert.True(t, ok)
-       seederTorrent.VerifyData()
-
-       cfg = TestingConfig()
-       cfg.Seed = true
-       cfg.DataDir, err = ioutil.TempDir("", "")
-       require.NoError(t, err)
-       defer os.RemoveAll(cfg.DataDir)
-       leecher, err := NewClient(cfg)
-       require.NoError(t, err)
-       defer leecher.Close()
-       defer testutil.ExportStatusWriter(leecher, "l")()
-
-       cfg = TestingConfig()
-       cfg.Seed = false
-       cfg.DataDir, err = ioutil.TempDir("", "")
-       require.NoError(t, err)
-       defer os.RemoveAll(cfg.DataDir)
-       leecherLeecher, _ := NewClient(cfg)
-       require.NoError(t, err)
-       defer leecherLeecher.Close()
-       defer testutil.ExportStatusWriter(leecherLeecher, "ll")()
-       leecherGreeting, ok, err := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
-               ret = TorrentSpecFromMetaInfo(mi)
-               ret.ChunkSize = 2
-               return
-       }())
-       require.NoError(t, err)
-       assert.True(t, ok)
-       llg, ok, err := leecherLeecher.AddTorrentSpec(func() (ret *TorrentSpec) {
-               ret = TorrentSpecFromMetaInfo(mi)
-               ret.ChunkSize = 3
-               return
-       }())
-       require.NoError(t, err)
-       assert.True(t, ok)
-       // Simultaneously DownloadAll in Leecher, and read the contents
-       // consecutively in LeecherLeecher. This non-deterministically triggered a
-       // case where the leecher wouldn't unchoke the LeecherLeecher.
-       var wg sync.WaitGroup
-       wg.Add(1)
-       go func() {
-               defer wg.Done()
-               r := llg.NewReader()
-               defer r.Close()
-               b, err := ioutil.ReadAll(r)
-               require.NoError(t, err)
-               assert.EqualValues(t, testutil.GreetingFileContents, b)
-       }()
-       done := make(chan struct{})
-       defer close(done)
-       go leecherGreeting.AddClientPeer(seeder)
-       go leecherGreeting.AddClientPeer(leecherLeecher)
-       wg.Add(1)
-       go func() {
-               defer wg.Done()
-               leecherGreeting.DownloadAll()
-               leecher.WaitAll()
-       }()
-       wg.Wait()
+       return storage.NewResourcePiecesOpts(
+               fc.AsResourceProvider(),
+               storage.ResourcePiecesOpts{
+                       LeaveIncompleteChunks: true,
+               },
+       )
 }
 
 func TestMergingTrackersByAddingSpecs(t *testing.T) {
-       cl, err := NewClient(TestingConfig())
+       cl, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
        defer cl.Close()
        spec := TorrentSpec{}
@@ -506,7 +169,7 @@ func TestMergingTrackersByAddingSpecs(t *testing.T) {
 
 // We read from a piece which is marked completed, but is missing data.
 func TestCompletedPieceWrongSize(t *testing.T) {
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        cfg.DefaultStorage = badStorage{}
        cl, err := NewClient(cfg)
        require.NoError(t, err)
@@ -529,20 +192,18 @@ func TestCompletedPieceWrongSize(t *testing.T) {
        assert.True(t, new)
        r := tt.NewReader()
        defer r.Close()
-       b, err = ioutil.ReadAll(r)
-       assert.Len(t, b, 13)
-       assert.NoError(t, err)
+       quicktest.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), quicktest.IsNil)
 }
 
 func BenchmarkAddLargeTorrent(b *testing.B) {
-       cfg := TestingConfig()
+       cfg := TestingConfig(b)
        cfg.DisableTCP = true
        cfg.DisableUTP = true
        cl, err := NewClient(cfg)
        require.NoError(b, err)
        defer cl.Close()
        b.ReportAllocs()
-       for range iter.N(b.N) {
+       for i := 0; i < b.N; i += 1 {
                t, err := cl.AddTorrentFromFile("testdata/bootstrap.dat.torrent")
                if err != nil {
                        b.Fatal(err)
@@ -554,7 +215,7 @@ func BenchmarkAddLargeTorrent(b *testing.B) {
 func TestResponsive(t *testing.T) {
        seederDataDir, mi := testutil.GreetingTestTorrent()
        defer os.RemoveAll(seederDataDir)
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        cfg.Seed = true
        cfg.DataDir = seederDataDir
        seeder, err := NewClient(cfg)
@@ -562,10 +223,52 @@ func TestResponsive(t *testing.T) {
        defer seeder.Close()
        seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
        seederTorrent.VerifyData()
-       leecherDataDir, err := ioutil.TempDir("", "")
+       leecherDataDir := t.TempDir()
+       cfg = TestingConfig(t)
+       cfg.DataDir = leecherDataDir
+       leecher, err := NewClient(cfg)
        require.Nil(t, err)
-       defer os.RemoveAll(leecherDataDir)
-       cfg = TestingConfig()
+       defer leecher.Close()
+       leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
+               ret = TorrentSpecFromMetaInfo(mi)
+               ret.ChunkSize = 2
+               return
+       }())
+       leecherTorrent.AddClientPeer(seeder)
+       reader := leecherTorrent.NewReader()
+       defer reader.Close()
+       reader.SetReadahead(0)
+       reader.SetResponsive()
+       b := make([]byte, 2)
+       _, err = reader.Seek(3, io.SeekStart)
+       require.NoError(t, err)
+       _, err = io.ReadFull(reader, b)
+       assert.Nil(t, err)
+       assert.EqualValues(t, "lo", string(b))
+       _, err = reader.Seek(11, io.SeekStart)
+       require.NoError(t, err)
+       n, err := io.ReadFull(reader, b)
+       assert.Nil(t, err)
+       assert.EqualValues(t, 2, n)
+       assert.EqualValues(t, "d\n", string(b))
+}
+
+// TestResponsive was the first test to fail if uTP is disabled and TCP sockets dial from the
+// listening port.
+func TestResponsiveTcpOnly(t *testing.T) {
+       seederDataDir, mi := testutil.GreetingTestTorrent()
+       defer os.RemoveAll(seederDataDir)
+       cfg := TestingConfig(t)
+       cfg.DisableUTP = true
+       cfg.Seed = true
+       cfg.DataDir = seederDataDir
+       seeder, err := NewClient(cfg)
+       require.Nil(t, err)
+       defer seeder.Close()
+       seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
+       seederTorrent.VerifyData()
+       leecherDataDir := t.TempDir()
+       cfg = TestingConfig(t)
        cfg.DataDir = leecherDataDir
        leecher, err := NewClient(cfg)
        require.Nil(t, err)
@@ -597,7 +300,7 @@ func TestResponsive(t *testing.T) {
 func TestTorrentDroppedDuringResponsiveRead(t *testing.T) {
        seederDataDir, mi := testutil.GreetingTestTorrent()
        defer os.RemoveAll(seederDataDir)
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        cfg.Seed = true
        cfg.DataDir = seederDataDir
        seeder, err := NewClient(cfg)
@@ -605,10 +308,8 @@ func TestTorrentDroppedDuringResponsiveRead(t *testing.T) {
        defer seeder.Close()
        seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
        seederTorrent.VerifyData()
-       leecherDataDir, err := ioutil.TempDir("", "")
-       require.Nil(t, err)
-       defer os.RemoveAll(leecherDataDir)
-       cfg = TestingConfig()
+       leecherDataDir := t.TempDir()
+       cfg = TestingConfig(t)
        cfg.DataDir = leecherDataDir
        leecher, err := NewClient(cfg)
        require.Nil(t, err)
@@ -629,26 +330,27 @@ func TestTorrentDroppedDuringResponsiveRead(t *testing.T) {
        _, err = io.ReadFull(reader, b)
        assert.Nil(t, err)
        assert.EqualValues(t, "lo", string(b))
-       go leecherTorrent.Drop()
        _, err = reader.Seek(11, io.SeekStart)
        require.NoError(t, err)
+       leecherTorrent.Drop()
        n, err := reader.Read(b)
        assert.EqualError(t, err, "torrent closed")
        assert.EqualValues(t, 0, n)
 }
 
-func TestDHTInheritBlocklist(t *testing.T) {
+func TestDhtInheritBlocklist(t *testing.T) {
        ipl := iplist.New(nil)
        require.NotNil(t, ipl)
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        cfg.IPBlocklist = ipl
        cfg.NoDHT = false
        cl, err := NewClient(cfg)
        require.NoError(t, err)
        defer cl.Close()
        numServers := 0
-       cl.eachDhtServer(func(s *dht.Server) {
-               assert.Equal(t, ipl, s.IPBlocklist())
+       cl.eachDhtServer(func(s DhtServer) {
+               t.Log(s)
+               assert.Equal(t, ipl, s.(AnacrolixDhtServerWrapper).Server.IPBlocklist())
                numServers++
        })
        assert.EqualValues(t, 2, numServers)
@@ -657,7 +359,7 @@ func TestDHTInheritBlocklist(t *testing.T) {
 // Check that stuff is merged in subsequent AddTorrentSpec for the same
 // infohash.
 func TestAddTorrentSpecMerging(t *testing.T) {
-       cl, err := NewClient(TestingConfig())
+       cl, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
        defer cl.Close()
        dir, mi := testutil.GreetingTestTorrent()
@@ -677,7 +379,7 @@ func TestAddTorrentSpecMerging(t *testing.T) {
 func TestTorrentDroppedBeforeGotInfo(t *testing.T) {
        dir, mi := testutil.GreetingTestTorrent()
        os.RemoveAll(dir)
-       cl, _ := NewClient(TestingConfig())
+       cl, _ := NewClient(TestingConfig(t))
        defer cl.Close()
        tt, _, _ := cl.AddTorrentSpec(&TorrentSpec{
                InfoHash: mi.HashInfoBytes(),
@@ -692,22 +394,19 @@ func TestTorrentDroppedBeforeGotInfo(t *testing.T) {
 }
 
 func writeTorrentData(ts *storage.Torrent, info metainfo.Info, b []byte) {
-       for i := range iter.N(info.NumPieces()) {
+       for i := 0; i < info.NumPieces(); i += 1 {
                p := info.Piece(i)
                ts.Piece(p).WriteAt(b[p.Offset():p.Offset()+p.Length()], 0)
        }
 }
 
 func testAddTorrentPriorPieceCompletion(t *testing.T, alreadyCompleted bool, csf func(*filecache.Cache) storage.ClientImpl) {
-       fileCacheDir, err := ioutil.TempDir("", "")
-       require.NoError(t, err)
-       defer os.RemoveAll(fileCacheDir)
+       fileCacheDir := t.TempDir()
        fileCache, err := filecache.NewCache(fileCacheDir)
        require.NoError(t, err)
        greetingDataTempDir, greetingMetainfo := testutil.GreetingTestTorrent()
        defer os.RemoveAll(greetingDataTempDir)
        filePieceStore := csf(fileCache)
-       defer filePieceStore.Close()
        info, err := greetingMetainfo.UnmarshalInfo()
        require.NoError(t, err)
        ih := greetingMetainfo.HashInfoBytes()
@@ -722,7 +421,7 @@ func testAddTorrentPriorPieceCompletion(t *testing.T, alreadyCompleted bool, csf
                        require.NoError(t, greetingData.Piece(p).MarkComplete())
                }
        }
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        // TODO: Disable network option?
        cfg.DisableTCP = true
        cfg.DisableUTP = true
@@ -738,9 +437,7 @@ func testAddTorrentPriorPieceCompletion(t *testing.T, alreadyCompleted bool, csf
        assert.Equal(t, alreadyCompleted, psrs[0].Complete)
        if alreadyCompleted {
                r := tt.NewReader()
-               b, err := ioutil.ReadAll(r)
-               assert.NoError(t, err)
-               assert.EqualValues(t, testutil.GreetingFileContents, b)
+               quicktest.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), quicktest.IsNil)
        }
 }
 
@@ -753,20 +450,19 @@ func TestAddTorrentPiecesNotAlreadyCompleted(t *testing.T) {
 }
 
 func TestAddMetainfoWithNodes(t *testing.T) {
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        cfg.ListenHost = func(string) string { return "" }
        cfg.NoDHT = false
-       cfg.DhtStartingNodes = func() ([]dht.Addr, error) { return nil, nil }
-       // For now, we want to just jam the nodes into the table, without
-       // verifying them first. Also the DHT code doesn't support mixing secure
-       // and insecure nodes if security is enabled (yet).
+       cfg.DhtStartingNodes = func(string) dht.StartingNodesGetter { return func() ([]dht.Addr, error) { return nil, nil } }
+       // For now, we want to just jam the nodes into the table, without verifying them first. Also the
+       // DHT code doesn't support mixing secure and insecure nodes if security is enabled (yet).
        // cfg.DHTConfig.NoSecurity = true
        cl, err := NewClient(cfg)
        require.NoError(t, err)
        defer cl.Close()
        sum := func() (ret int64) {
-               cl.eachDhtServer(func(s *dht.Server) {
-                       ret += s.Stats().OutboundQueriesAttempted
+               cl.eachDhtServer(func(s DhtServer) {
+                       ret += s.Stats().(dht.ServerStats).OutboundQueriesAttempted
                })
                return
        }
@@ -791,18 +487,16 @@ type testDownloadCancelParams struct {
 func testDownloadCancel(t *testing.T, ps testDownloadCancelParams) {
        greetingTempDir, mi := testutil.GreetingTestTorrent()
        defer os.RemoveAll(greetingTempDir)
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        cfg.Seed = true
        cfg.DataDir = greetingTempDir
        seeder, err := NewClient(cfg)
        require.NoError(t, err)
        defer seeder.Close()
-       defer testutil.ExportStatusWriter(seeder, "s")()
+       defer testutil.ExportStatusWriter(seeder, "s", t)()
        seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
        seederTorrent.VerifyData()
-       leecherDataDir, err := ioutil.TempDir("", "")
-       require.NoError(t, err)
-       defer os.RemoveAll(leecherDataDir)
+       leecherDataDir := t.TempDir()
        fc, err := filecache.NewCache(leecherDataDir)
        require.NoError(t, err)
        if ps.SetLeecherStorageCapacity {
@@ -813,7 +507,7 @@ func testDownloadCancel(t *testing.T, ps testDownloadCancelParams) {
        leecher, err := NewClient(cfg)
        require.NoError(t, err)
        defer leecher.Close()
-       defer testutil.ExportStatusWriter(leecher, "l")()
+       defer testutil.ExportStatusWriter(leecher, "l", t)()
        leecherGreeting, new, err := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
                ret = TorrentSpecFromMetaInfo(mi)
                ret.ChunkSize = 2
@@ -827,7 +521,7 @@ func testDownloadCancel(t *testing.T, ps testDownloadCancelParams) {
        leecherGreeting.cl.lock()
        leecherGreeting.downloadPiecesLocked(0, leecherGreeting.numPieces())
        if ps.Cancel {
-               leecherGreeting.cancelPiecesLocked(0, leecherGreeting.NumPieces())
+               leecherGreeting.cancelPiecesLocked(0, leecherGreeting.NumPieces(), "")
        }
        leecherGreeting.cl.unlock()
        done := make(chan struct{})
@@ -842,8 +536,7 @@ func testDownloadCancel(t *testing.T, ps testDownloadCancelParams) {
                }
        }()
        for !reflect.DeepEqual(completes, expected) {
-               _v := <-psc.Values
-               v := _v.(PieceStateChange)
+               v := <-psc.Values
                completes[v.Index] = v.Complete
        }
 }
@@ -860,7 +553,9 @@ func TestTorrentDownloadAllThenCancel(t *testing.T) {
 
 // Ensure that it's an error for a peer to send an invalid have message.
 func TestPeerInvalidHave(t *testing.T) {
-       cl, err := NewClient(TestingConfig())
+       cfg := TestingConfig(t)
+       cfg.DropMutuallyCompletePeers = false
+       cl, err := NewClient(cfg)
        require.NoError(t, err)
        defer cl.Close()
        info := metainfo.Info{
@@ -878,9 +573,14 @@ func TestPeerInvalidHave(t *testing.T) {
        require.NoError(t, err)
        assert.True(t, _new)
        defer tt.Drop()
-       cn := &connection{
-               t: tt,
-       }
+       cn := &PeerConn{Peer: Peer{
+               t:         tt,
+               callbacks: &cfg.Callbacks,
+       }}
+       tt.conns[cn] = struct{}{}
+       cn.peerImpl = cn
+       cl.lock()
+       defer cl.unlock()
        assert.NoError(t, cn.peerSentHave(0))
        assert.Error(t, cn.peerSentHave(1))
 }
@@ -888,10 +588,11 @@ func TestPeerInvalidHave(t *testing.T) {
 func TestPieceCompletedInStorageButNotClient(t *testing.T) {
        greetingTempDir, greetingMetainfo := testutil.GreetingTestTorrent()
        defer os.RemoveAll(greetingTempDir)
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        cfg.DataDir = greetingTempDir
-       seeder, err := NewClient(TestingConfig())
+       seeder, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
+       defer seeder.Close()
        seeder.AddTorrentSpec(&TorrentSpec{
                InfoBytes: greetingMetainfo.InfoBytes,
        })
@@ -900,19 +601,19 @@ func TestPieceCompletedInStorageButNotClient(t *testing.T) {
 // Check that when the listen port is 0, all the protocols listened on have
 // the same port, and it isn't zero.
 func TestClientDynamicListenPortAllProtocols(t *testing.T) {
-       cl, err := NewClient(TestingConfig())
+       cl, err := NewClient(TestingConfig(t))
        require.NoError(t, err)
        defer cl.Close()
        port := cl.LocalPort()
        assert.NotEqual(t, 0, port)
-       cl.eachListener(func(s socket) bool {
+       cl.eachListener(func(s Listener) bool {
                assert.Equal(t, port, missinggo.AddrPort(s.Addr()))
                return true
        })
 }
 
 func TestClientDynamicListenTCPOnly(t *testing.T) {
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        cfg.DisableUTP = true
        cfg.DisableTCP = false
        cl, err := NewClient(cfg)
@@ -922,7 +623,7 @@ func TestClientDynamicListenTCPOnly(t *testing.T) {
 }
 
 func TestClientDynamicListenUTPOnly(t *testing.T) {
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        cfg.DisableTCP = true
        cfg.DisableUTP = false
        cl, err := NewClient(cfg)
@@ -943,16 +644,16 @@ func totalConns(tts []*Torrent) (ret int) {
 func TestSetMaxEstablishedConn(t *testing.T) {
        var tts []*Torrent
        ih := testutil.GreetingMetaInfo().HashInfoBytes()
-       cfg := TestingConfig()
+       cfg := TestingConfig(t)
        cfg.DisableAcceptRateLimiting = true
-       cfg.dropDuplicatePeerIds = true
-       for i := range iter.N(3) {
+       cfg.DropDuplicatePeerIds = true
+       for i := 0; i < 3; i += 1 {
                cl, err := NewClient(cfg)
                require.NoError(t, err)
                defer cl.Close()
                tt, _ := cl.AddTorrentInfoHash(ih)
                tt.SetMaxEstablishedConns(2)
-               defer testutil.ExportStatusWriter(cl, fmt.Sprintf("%d", i))()
+               defer testutil.ExportStatusWriter(cl, fmt.Sprintf("%d", i), t)()
                tts = append(tts, tt)
        }
        addPeers := func() {
@@ -986,8 +687,8 @@ func TestSetMaxEstablishedConn(t *testing.T) {
 
 // Creates a file containing its own name as data. Make a metainfo from that, adds it to the given
 // client, and returns a magnet link.
-func makeMagnet(t *testing.T, cl *Client, dir string, name string) string {
-       os.MkdirAll(dir, 0770)
+func makeMagnet(t *testing.T, cl *Client, dir, name string) string {
+       os.MkdirAll(dir, 0o770)
        file, err := os.Create(filepath.Join(dir, name))
        require.NoError(t, err)
        file.Write([]byte(name))
@@ -999,7 +700,7 @@ func makeMagnet(t *testing.T, cl *Client, dir string, name string) string {
        require.NoError(t, err)
        mi.InfoBytes, err = bencode.Marshal(info)
        require.NoError(t, err)
-       magnet := mi.Magnet(name, mi.HashInfoBytes()).String()
+       magnet := mi.Magnet(nil, &info).String()
        tr, err := cl.AddTorrent(&mi)
        require.NoError(t, err)
        require.True(t, tr.Seeding())
@@ -1023,16 +724,16 @@ func TestMultipleTorrentsWithEncryption(t *testing.T) {
 
 // Test that the leecher can download a torrent in its entirety from the seeder. Note that the
 // seeder config is done first.
-func testSeederLeecherPair(t *testing.T, seeder func(*ClientConfig), leecher func(*ClientConfig)) {
-       cfg := TestingConfig()
+func testSeederLeecherPair(t *testing.T, seeder, leecher func(*ClientConfig)) {
+       cfg := TestingConfig(t)
        cfg.Seed = true
        cfg.DataDir = filepath.Join(cfg.DataDir, "server")
-       os.Mkdir(cfg.DataDir, 0755)
+       os.Mkdir(cfg.DataDir, 0o755)
        seeder(cfg)
        server, err := NewClient(cfg)
        require.NoError(t, err)
        defer server.Close()
-       defer testutil.ExportStatusWriter(server, "s")()
+       defer testutil.ExportStatusWriter(server, "s", t)()
        magnet1 := makeMagnet(t, server, cfg.DataDir, "test1")
        // Extra torrents are added to test the seeder having to match incoming obfuscated headers
        // against more than one torrent. See issue #114
@@ -1040,13 +741,13 @@ func testSeederLeecherPair(t *testing.T, seeder func(*ClientConfig), leecher fun
        for i := 0; i < 100; i++ {
                makeMagnet(t, server, cfg.DataDir, fmt.Sprintf("test%d", i+2))
        }
-       cfg = TestingConfig()
+       cfg = TestingConfig(t)
        cfg.DataDir = filepath.Join(cfg.DataDir, "client")
        leecher(cfg)
        client, err := NewClient(cfg)
        require.NoError(t, err)
        defer client.Close()
-       defer testutil.ExportStatusWriter(client, "c")()
+       defer testutil.ExportStatusWriter(client, "c", t)()
        tr, err := client.AddMagnet(magnet1)
        require.NoError(t, err)
        tr.AddClientPeer(server)
@@ -1087,18 +788,22 @@ func TestObfuscatedHeaderFallbackSeederRequiresLeecherPrefersNot(t *testing.T) {
 }
 
 func TestClientAddressInUse(t *testing.T) {
-       s, _ := NewUtpSocket("udp", ":50007", nil)
+       s, _ := NewUtpSocket("udp", "localhost:50007", nil, log.Default)
        if s != nil {
                defer s.Close()
        }
-       cfg := TestingConfig().SetListenAddr(":50007")
+       cfg := TestingConfig(t).SetListenAddr("localhost:50007")
+       cfg.DisableUTP = false
        cl, err := NewClient(cfg)
+       if err == nil {
+               assert.Nil(t, cl.Close())
+       }
        require.Error(t, err)
        require.Nil(t, cl)
 }
 
 func TestClientHasDhtServersWhenUtpDisabled(t *testing.T) {
-       cc := TestingConfig()
+       cc := TestingConfig(t)
        cc.DisableUTP = true
        cc.NoDHT = false
        cl, err := NewClient(cc)
@@ -1107,27 +812,98 @@ func TestClientHasDhtServersWhenUtpDisabled(t *testing.T) {
        assert.NotEmpty(t, cl.DhtServers())
 }
 
-func TestIssue335(t *testing.T) {
-       dir, mi := testutil.GreetingTestTorrent()
-       defer os.RemoveAll(dir)
-       cfg := TestingConfig()
-       cfg.Seed = false
-       cfg.Debug = true
-       cfg.DataDir = dir
-       comp, err := storage.NewBoltPieceCompletion(dir)
-       require.NoError(t, err)
-       defer comp.Close()
-       cfg.DefaultStorage = storage.NewMMapWithCompletion(dir, comp)
+func TestClientDisabledImplicitNetworksButDhtEnabled(t *testing.T) {
+       cfg := TestingConfig(t)
+       cfg.DisableTCP = true
+       cfg.DisableUTP = true
+       cfg.NoDHT = false
        cl, err := NewClient(cfg)
        require.NoError(t, err)
        defer cl.Close()
-       tor, new, err := cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
-       require.NoError(t, err)
-       assert.True(t, new)
-       require.True(t, cl.WaitAll())
-       tor.Drop()
-       tor, new, err = cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
-       require.NoError(t, err)
-       assert.True(t, new)
-       require.True(t, cl.WaitAll())
+       assert.Empty(t, cl.listeners)
+       assert.NotEmpty(t, cl.DhtServers())
+}
+
+func TestBadPeerIpPort(t *testing.T) {
+       for _, tc := range []struct {
+               title      string
+               ip         net.IP
+               port       int
+               expectedOk bool
+               setup      func(*Client)
+       }{
+               {"empty both", nil, 0, true, func(*Client) {}},
+               {"empty/nil ip", nil, 6666, true, func(*Client) {}},
+               {
+                       "empty port",
+                       net.ParseIP("127.0.0.1/32"),
+                       0, true,
+                       func(*Client) {},
+               },
+               {
+                       "in doppleganger addresses",
+                       net.ParseIP("127.0.0.1/32"),
+                       2322,
+                       true,
+                       func(cl *Client) {
+                               cl.dopplegangerAddrs["10.0.0.1:2322"] = struct{}{}
+                       },
+               },
+               {
+                       "in IP block list",
+                       net.ParseIP("10.0.0.1"),
+                       2322,
+                       true,
+                       func(cl *Client) {
+                               cl.ipBlockList = iplist.New([]iplist.Range{
+                                       {First: net.ParseIP("10.0.0.1"), Last: net.ParseIP("10.0.0.255")},
+                               })
+                       },
+               },
+               {
+                       "in bad peer IPs",
+                       net.ParseIP("10.0.0.1"),
+                       2322,
+                       true,
+                       func(cl *Client) {
+                               ipAddr, ok := netip.AddrFromSlice(net.ParseIP("10.0.0.1"))
+                               require.True(t, ok)
+                               cl.badPeerIPs = map[netip.Addr]struct{}{}
+                               cl.badPeerIPs[ipAddr] = struct{}{}
+                       },
+               },
+               {
+                       "good",
+                       net.ParseIP("10.0.0.1"),
+                       2322,
+                       false,
+                       func(cl *Client) {},
+               },
+       } {
+               t.Run(tc.title, func(t *testing.T) {
+                       cfg := TestingConfig(t)
+                       cfg.DisableTCP = true
+                       cfg.DisableUTP = true
+                       cfg.NoDHT = false
+                       cl, err := NewClient(cfg)
+                       require.NoError(t, err)
+                       defer cl.Close()
+
+                       tc.setup(cl)
+                       require.Equal(t, tc.expectedOk, cl.badPeerIPPort(tc.ip, tc.port))
+               })
+       }
+}
+
+// https://github.com/anacrolix/torrent/issues/837
+func TestClientConfigSetHandlerNotIgnored(t *testing.T) {
+       cfg := TestingConfig(t)
+       cfg.Logger.SetHandlers(log.DiscardHandler)
+       c := qt.New(t)
+       cl, err := NewClient(cfg)
+       c.Assert(err, qt.IsNil)
+       defer cl.Close()
+       c.Assert(cl.logger.Handlers, qt.HasLen, 1)
+       h := cl.logger.Handlers[0].(log.StreamHandler)
+       c.Check(h.W, qt.Equals, io.Discard)
 }