github.com/anacrolix/torrent@v1.61.0/test/leecher-storage.go (about)

     1  package test
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"runtime"
     8  	"testing"
     9  	"testing/iotest"
    10  
    11  	"github.com/anacrolix/missinggo/v2/bitmap"
    12  	qt "github.com/go-quicktest/qt"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	"golang.org/x/time/rate"
    16  
    17  	"github.com/anacrolix/torrent"
    18  	"github.com/anacrolix/torrent/internal/testutil"
    19  	"github.com/anacrolix/torrent/storage"
    20  )
    21  
    22  type LeecherStorageTestCase struct {
    23  	Name       string
    24  	Factory    StorageFactory
    25  	GoMaxProcs int
    26  }
    27  
    28  type StorageFactory func(string) storage.ClientImplCloser
    29  
    30  func TestLeecherStorage(t *testing.T, ls LeecherStorageTestCase) {
    31  	// Seeder storage
    32  	for _, ss := range []struct {
    33  		name string
    34  		f    StorageFactory
    35  	}{
    36  		{"File", storage.NewFile},
    37  		{"Mmap", storage.NewMMap},
    38  	} {
    39  		t.Run(fmt.Sprintf("%sSeederStorage", ss.name), func(t *testing.T) {
    40  			for _, responsive := range []bool{false, true} {
    41  				t.Run(fmt.Sprintf("Responsive=%v", responsive), func(t *testing.T) {
    42  					t.Run("NoReadahead", func(t *testing.T) {
    43  						testClientTransfer(t, testClientTransferParams{
    44  							Responsive:     responsive,
    45  							SeederStorage:  ss.f,
    46  							LeecherStorage: ls.Factory,
    47  							GOMAXPROCS:     ls.GoMaxProcs,
    48  						})
    49  					})
    50  					for _, readahead := range []int64{-1, 0, 1, 2, 9, 20} {
    51  						t.Run(fmt.Sprintf("readahead=%v", readahead), func(t *testing.T) {
    52  							testClientTransfer(t, testClientTransferParams{
    53  								SeederStorage:  ss.f,
    54  								Responsive:     responsive,
    55  								SetReadahead:   true,
    56  								Readahead:      readahead,
    57  								LeecherStorage: ls.Factory,
    58  								GOMAXPROCS:     ls.GoMaxProcs,
    59  							})
    60  						})
    61  					}
    62  				})
    63  			}
    64  		})
    65  	}
    66  }
    67  
    68  type ConfigureClient struct {
    69  	Config func(cfg *torrent.ClientConfig)
    70  	Client func(cl *torrent.Client)
    71  }
    72  
    73  type testClientTransferParams struct {
    74  	Responsive     bool
    75  	Readahead      int64
    76  	SetReadahead   bool
    77  	LeecherStorage func(string) storage.ClientImplCloser
    78  	// TODO: Use a generic option type. This is the capacity of the leecher storage for determining
    79  	// whether it's possible for the leecher to be Complete. 0 currently means no limit.
    80  	LeecherStorageCapacity     int64
    81  	SeederStorage              func(string) storage.ClientImplCloser
    82  	SeederUploadRateLimiter    *rate.Limiter
    83  	LeecherDownloadRateLimiter *rate.Limiter
    84  	ConfigureSeeder            ConfigureClient
    85  	ConfigureLeecher           ConfigureClient
    86  	GOMAXPROCS                 int
    87  
    88  	LeecherStartsWithoutMetadata bool
    89  }
    90  
    91  // Creates a seeder and a leecher, and ensures the data transfers when a read
    92  // is attempted on the leecher.
    93  func testClientTransfer(t *testing.T, ps testClientTransferParams) {
    94  	prevGOMAXPROCS := runtime.GOMAXPROCS(ps.GOMAXPROCS)
    95  	newGOMAXPROCS := prevGOMAXPROCS
    96  	if ps.GOMAXPROCS > 0 {
    97  		newGOMAXPROCS = ps.GOMAXPROCS
    98  	}
    99  	defer func() {
   100  		qt.Check(t, qt.ContentEquals(runtime.GOMAXPROCS(prevGOMAXPROCS), newGOMAXPROCS))
   101  	}()
   102  
   103  	greetingTempDir, mi := testutil.GreetingTestTorrent()
   104  	defer os.RemoveAll(greetingTempDir)
   105  	// Create seeder and a Torrent.
   106  	cfg := torrent.TestingConfig(t)
   107  	// cfg.Debug = true
   108  	cfg.Seed = true
   109  	// Less than a piece, more than a single request.
   110  	cfg.MaxAllocPeerRequestDataPerConn = 4
   111  	// Some test instances don't like this being on, even when there's no cache involved.
   112  	cfg.DropMutuallyCompletePeers = false
   113  	if ps.SeederUploadRateLimiter != nil {
   114  		cfg.UploadRateLimiter = ps.SeederUploadRateLimiter
   115  	}
   116  	// cfg.ListenAddr = "localhost:4000"
   117  	if ps.SeederStorage != nil {
   118  		storage := ps.SeederStorage(greetingTempDir)
   119  		defer storage.Close()
   120  		cfg.DefaultStorage = storage
   121  	} else {
   122  		cfg.DataDir = greetingTempDir
   123  	}
   124  	if ps.ConfigureSeeder.Config != nil {
   125  		ps.ConfigureSeeder.Config(cfg)
   126  	}
   127  	seeder, err := torrent.NewClient(cfg)
   128  	require.NoError(t, err)
   129  	if ps.ConfigureSeeder.Client != nil {
   130  		ps.ConfigureSeeder.Client(seeder)
   131  	}
   132  	defer testutil.ExportStatusWriter(seeder, "s", t)()
   133  	seederTorrent, _, _ := seeder.AddTorrentSpec(torrent.TorrentSpecFromMetaInfo(mi))
   134  	// Run a Stats right after Closing the Client. This will trigger the Stats
   135  	// panic in #214 caused by RemoteAddr on Closed uTP sockets.
   136  	defer seederTorrent.Stats()
   137  	defer seeder.Close()
   138  	// Adding a torrent and setting the info should trigger piece checks for everything
   139  	// automatically. Wait until the seed Torrent agrees that everything is available.
   140  	<-seederTorrent.Complete().On()
   141  	// Create leecher and a Torrent.
   142  	leecherDataDir := t.TempDir()
   143  	cfg = torrent.TestingConfig(t)
   144  	// See the seeder client config comment.
   145  	cfg.DropMutuallyCompletePeers = false
   146  	if ps.LeecherStorage == nil {
   147  		cfg.DataDir = leecherDataDir
   148  	} else {
   149  		storage := ps.LeecherStorage(leecherDataDir)
   150  		defer storage.Close()
   151  		cfg.DefaultStorage = storage
   152  	}
   153  	if ps.LeecherDownloadRateLimiter != nil {
   154  		cfg.DownloadRateLimiter = ps.LeecherDownloadRateLimiter
   155  	}
   156  	cfg.Seed = false
   157  	// cfg.Debug = true
   158  	if ps.ConfigureLeecher.Config != nil {
   159  		ps.ConfigureLeecher.Config(cfg)
   160  	}
   161  	leecher, err := torrent.NewClient(cfg)
   162  	require.NoError(t, err)
   163  	defer leecher.Close()
   164  	if ps.ConfigureLeecher.Client != nil {
   165  		ps.ConfigureLeecher.Client(leecher)
   166  	}
   167  	defer testutil.ExportStatusWriter(leecher, "l", t)()
   168  	leecherTorrent, new, err := leecher.AddTorrentSpec(func() (ret *torrent.TorrentSpec) {
   169  		ret = torrent.TorrentSpecFromMetaInfo(mi)
   170  		ret.ChunkSize = 2
   171  		if ps.LeecherStartsWithoutMetadata {
   172  			ret.InfoBytes = nil
   173  		}
   174  		return
   175  	}())
   176  	require.NoError(t, err)
   177  	assert.False(t, leecherTorrent.Complete().Bool())
   178  	assert.True(t, new)
   179  
   180  	//// This was used when observing coalescing of piece state changes.
   181  	//logPieceStateChanges(leecherTorrent)
   182  
   183  	// Now do some things with leecher and seeder.
   184  	added := leecherTorrent.AddClientPeer(seeder)
   185  	assert.False(t, leecherTorrent.Seeding())
   186  	// The leecher will use peers immediately if it doesn't have the metadata. Otherwise, they
   187  	// should be sitting idle until we demand data.
   188  	if !ps.LeecherStartsWithoutMetadata {
   189  		assert.EqualValues(t, added, leecherTorrent.Stats().PendingPeers)
   190  	}
   191  	if ps.LeecherStartsWithoutMetadata {
   192  		<-leecherTorrent.GotInfo()
   193  	}
   194  	r := leecherTorrent.NewReader()
   195  	defer r.Close()
   196  	go leecherTorrent.SetInfoBytes(mi.InfoBytes)
   197  	if ps.Responsive {
   198  		r.SetResponsive()
   199  	}
   200  	if ps.SetReadahead {
   201  		r.SetReadahead(ps.Readahead)
   202  	}
   203  	assertReadAllGreeting(t, r)
   204  	info, err := mi.UnmarshalInfo()
   205  	require.NoError(t, err)
   206  	canComplete := ps.LeecherStorageCapacity == 0 || ps.LeecherStorageCapacity >= info.TotalLength()
   207  	if !canComplete {
   208  		// Reading from a cache doesn't refresh older pieces until we fail to read those, so we need
   209  		// to force a refresh since we just read the contents from start to finish.
   210  		go leecherTorrent.VerifyData()
   211  	}
   212  	if canComplete {
   213  		<-leecherTorrent.Complete().On()
   214  	} else {
   215  		<-leecherTorrent.Complete().Off()
   216  	}
   217  	assert.NotEmpty(t, seederTorrent.PeerConns())
   218  	leecherPeerConns := leecherTorrent.PeerConns()
   219  	if cfg.DropMutuallyCompletePeers {
   220  		// I don't think we can assume it will be empty already, due to timing.
   221  		// assert.Empty(t, leecherPeerConns)
   222  	} else {
   223  		assert.NotEmpty(t, leecherPeerConns)
   224  	}
   225  	foundSeeder := false
   226  	for _, pc := range leecherPeerConns {
   227  		completed := pc.PeerPieces().GetCardinality()
   228  		t.Logf("peer conn %v has %v completed pieces", pc, completed)
   229  		if completed == bitmap.BitRange(leecherTorrent.Info().NumPieces()) {
   230  			foundSeeder = true
   231  		}
   232  	}
   233  	if !foundSeeder {
   234  		t.Errorf("didn't find seeder amongst leecher peer conns")
   235  	}
   236  
   237  	seederStats := seederTorrent.Stats()
   238  	assert.True(t, 13 <= seederStats.BytesWrittenData.Int64())
   239  	assert.True(t, 8 <= seederStats.ChunksWritten.Int64())
   240  
   241  	leecherStats := leecherTorrent.Stats()
   242  	assert.True(t, 13 <= leecherStats.BytesReadData.Int64())
   243  	assert.True(t, 8 <= leecherStats.ChunksRead.Int64())
   244  
   245  	// Try reading through again for the cases where the torrent data size
   246  	// exceeds the size of the cache.
   247  	assertReadAllGreeting(t, r)
   248  }
   249  
   250  func assertReadAllGreeting(t *testing.T, r io.ReadSeeker) {
   251  	pos, err := r.Seek(0, io.SeekStart)
   252  	assert.NoError(t, err)
   253  	assert.EqualValues(t, 0, pos)
   254  	qt.Check(t, qt.IsNil(iotest.TestReader(r, []byte(testutil.GreetingFileContents))))
   255  }