github.com/anacrolix/torrent@v1.61.0/fs/torrentfs_test.go (about)

     1  //go:build !windows
     2  
     3  package torrentfs
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"log"
    10  	"net"
    11  	_ "net/http/pprof"
    12  	"os"
    13  	"path/filepath"
    14  	"testing"
    15  	"time"
    16  
    17  	_ "github.com/anacrolix/envpprof"
    18  	"github.com/anacrolix/fuse"
    19  	fusefs "github.com/anacrolix/fuse/fs"
    20  	"github.com/anacrolix/missinggo/v2"
    21  	"github.com/pkg/errors"
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/require"
    24  
    25  	"github.com/anacrolix/torrent"
    26  	"github.com/anacrolix/torrent/internal/testutil"
    27  	"github.com/anacrolix/torrent/metainfo"
    28  	"github.com/anacrolix/torrent/storage"
    29  )
    30  
    31  func init() {
    32  	log.SetFlags(log.Flags() | log.Lshortfile)
    33  }
    34  
    35  func TestTCPAddrString(t *testing.T) {
    36  	l, err := net.Listen("tcp4", "localhost:0")
    37  	if err != nil {
    38  		t.Fatal(err)
    39  	}
    40  	defer l.Close()
    41  	c, err := net.Dial("tcp", l.Addr().String())
    42  	if err != nil {
    43  		t.Fatal(err)
    44  	}
    45  	defer c.Close()
    46  	ras := c.RemoteAddr().String()
    47  	ta := &net.TCPAddr{
    48  		IP:   net.IPv4(127, 0, 0, 1),
    49  		Port: missinggo.AddrPort(l.Addr()),
    50  	}
    51  	s := ta.String()
    52  	if ras != s {
    53  		t.FailNow()
    54  	}
    55  }
    56  
    57  type testLayout struct {
    58  	BaseDir   string
    59  	MountDir  string
    60  	Completed string
    61  	Metainfo  *metainfo.MetaInfo
    62  }
    63  
    64  func (tl *testLayout) Destroy() error {
    65  	return os.RemoveAll(tl.BaseDir)
    66  }
    67  
    68  func newGreetingLayout(t *testing.T) (tl testLayout, err error) {
    69  	tl.BaseDir = t.TempDir()
    70  	tl.Completed = filepath.Join(tl.BaseDir, "completed")
    71  	os.Mkdir(tl.Completed, 0o777)
    72  	tl.MountDir = filepath.Join(tl.BaseDir, "mnt")
    73  	os.Mkdir(tl.MountDir, 0o777)
    74  	testutil.CreateDummyTorrentData(tl.Completed)
    75  	tl.Metainfo = testutil.GreetingMetaInfo()
    76  	return
    77  }
    78  
    79  // Unmount without first killing the FUSE connection while there are FUSE
    80  // operations blocked inside the filesystem code.
    81  func TestUnmountWedged(t *testing.T) {
    82  	layout, err := newGreetingLayout(t)
    83  	require.NoError(t, err)
    84  	defer func() {
    85  		err := layout.Destroy()
    86  		if err != nil {
    87  			t.Log(err)
    88  		}
    89  	}()
    90  	cfg := torrent.NewDefaultClientConfig()
    91  	cfg.DataDir = filepath.Join(layout.BaseDir, "incomplete")
    92  	cfg.DisableTrackers = true
    93  	cfg.NoDHT = true
    94  	cfg.DisableTCP = true
    95  	cfg.DisableUTP = true
    96  	client, err := torrent.NewClient(cfg)
    97  	require.NoError(t, err)
    98  	defer client.Close()
    99  	tt, err := client.AddTorrent(layout.Metainfo)
   100  	require.NoError(t, err)
   101  	fs := New(client)
   102  	fuseConn, err := fuse.Mount(layout.MountDir)
   103  	if err != nil {
   104  		if err.Error() == "fusermount: exit status 1" {
   105  			t.Skip(err)
   106  		}
   107  		if !errors.Is(err, fuse.ErrOSXFUSENotFound) {
   108  			t.Fatal(err)
   109  		}
   110  	}
   111  	go func() {
   112  		server := fusefs.New(fuseConn, &fusefs.Config{
   113  			Debug: func(msg interface{}) {
   114  				t.Log(msg)
   115  			},
   116  		})
   117  		server.Serve(fs)
   118  	}()
   119  	<-fuseConn.Ready
   120  	if err := fuseConn.MountError; err != nil {
   121  		t.Fatalf("mount error: %s", err)
   122  	}
   123  	ctx, cancel := context.WithCancel(context.Background())
   124  	// Read the greeting file, though it will never be available. This should
   125  	// "wedge" FUSE, requiring the fs object to be forcibly destroyed. The
   126  	// read call will return with a FS error.
   127  	go func() {
   128  		<-ctx.Done()
   129  		fs.mu.Lock()
   130  		fs.event.Broadcast()
   131  		fs.mu.Unlock()
   132  	}()
   133  	go func() {
   134  		defer cancel()
   135  		_, err := ioutil.ReadFile(filepath.Join(layout.MountDir, tt.Info().BestName()))
   136  		require.Error(t, err)
   137  	}()
   138  
   139  	// Wait until the read has blocked inside the filesystem code.
   140  	fs.mu.Lock()
   141  	for fs.blockedReads != 1 && ctx.Err() == nil {
   142  		fs.event.Wait()
   143  	}
   144  	fs.mu.Unlock()
   145  
   146  	fs.Destroy()
   147  
   148  	for {
   149  		err = fuse.Unmount(layout.MountDir)
   150  		if err != nil {
   151  			t.Logf("error unmounting: %s", err)
   152  			time.Sleep(time.Millisecond)
   153  		} else {
   154  			break
   155  		}
   156  	}
   157  
   158  	err = fuseConn.Close()
   159  	assert.NoError(t, err)
   160  }
   161  
   162  func TestDownloadOnDemand(t *testing.T) {
   163  	layout, err := newGreetingLayout(t)
   164  	require.NoError(t, err)
   165  	defer layout.Destroy()
   166  	cfg := torrent.NewDefaultClientConfig()
   167  	cfg.DataDir = layout.Completed
   168  	cfg.DisableTrackers = true
   169  	cfg.NoDHT = true
   170  	cfg.Seed = true
   171  	cfg.ListenPort = 0
   172  	cfg.ListenHost = torrent.LoopbackListenHost
   173  	seeder, err := torrent.NewClient(cfg)
   174  	require.NoError(t, err)
   175  	defer seeder.Close()
   176  	defer testutil.ExportStatusWriter(seeder, "s", t)()
   177  	// Just to mix things up, the seeder starts with the data, but the leecher
   178  	// starts with the metainfo.
   179  	seederTorrent, err := seeder.AddMagnet(fmt.Sprintf("magnet:?xt=urn:btih:%s", layout.Metainfo.HashInfoBytes().HexString()))
   180  	require.NoError(t, err)
   181  	go func() {
   182  		// Wait until we get the metainfo, then check for the data.
   183  		<-seederTorrent.GotInfo()
   184  		seederTorrent.VerifyData()
   185  	}()
   186  	cfg = torrent.NewDefaultClientConfig()
   187  	cfg.DisableTrackers = true
   188  	cfg.NoDHT = true
   189  	cfg.DisableTCP = true
   190  	cfg.DefaultStorage = storage.NewMMap(filepath.Join(layout.BaseDir, "download"))
   191  	cfg.ListenHost = torrent.LoopbackListenHost
   192  	cfg.ListenPort = 0
   193  	leecher, err := torrent.NewClient(cfg)
   194  	require.NoError(t, err)
   195  	testutil.ExportStatusWriter(leecher, "l", t)()
   196  	defer leecher.Close()
   197  	leecherTorrent, err := leecher.AddTorrent(layout.Metainfo)
   198  	require.NoError(t, err)
   199  	leecherTorrent.AddClientPeer(seeder)
   200  	fs := New(leecher)
   201  	defer fs.Destroy()
   202  	root, _ := fs.Root()
   203  	node, _ := root.(fusefs.NodeStringLookuper).Lookup(context.Background(), "greeting")
   204  	var attr fuse.Attr
   205  	node.Attr(context.Background(), &attr)
   206  	size := attr.Size
   207  	data := make([]byte, size)
   208  	h, err := node.(fusefs.NodeOpener).Open(context.TODO(), nil, nil)
   209  	require.NoError(t, err)
   210  
   211  	// torrent.Reader.Read no longer tries to fill the entire read buffer, so this is a ReadFull for
   212  	// fusefs.
   213  	var n int
   214  	for n < len(data) {
   215  		resp := fuse.ReadResponse{Data: data[n:]}
   216  		err := h.(fusefs.HandleReader).Read(context.Background(), &fuse.ReadRequest{
   217  			Size:   int(size) - n,
   218  			Offset: int64(n),
   219  		}, &resp)
   220  		assert.NoError(t, err)
   221  		n += len(resp.Data)
   222  	}
   223  
   224  	assert.EqualValues(t, testutil.GreetingFileContents, data)
   225  }
   226  
   227  func TestIsSubPath(t *testing.T) {
   228  	for _, case_ := range []struct {
   229  		parent, child string
   230  		is            bool
   231  	}{
   232  		{"", "", false},
   233  		{"", "/", true},
   234  		{"", "a", true},
   235  		{"a/b", "a/bc", false},
   236  		{"a/b", "a/b", false},
   237  		{"a/b", "a/b/c", true},
   238  		{"a/b", "a//b", false},
   239  	} {
   240  		assert.Equal(t, case_.is, isSubPath(case_.parent, case_.child))
   241  	}
   242  }