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 }