]> Sergey Matveev's repositories - btrtrc.git/blobdiff - fs/torrentfs_test.go
Drop support for go 1.20
[btrtrc.git] / fs / torrentfs_test.go
index 8cd8ebe55a210477e7e68f92ce6576de5579c794..097f1bb2377449d1c4300e4d8496a1f90806ad11 100644 (file)
@@ -1,38 +1,36 @@
 package torrentfs
 
 import (
-       "bytes"
+       "context"
        "fmt"
        "io/ioutil"
        "log"
        "net"
-       "net/http"
        _ "net/http/pprof"
        "os"
        "path/filepath"
        "testing"
        "time"
 
-       "bitbucket.org/anacrolix/go.torrent"
-       "bitbucket.org/anacrolix/go.torrent/testutil"
-       "bitbucket.org/anacrolix/go.torrent/util"
-       "github.com/anacrolix/libtorgo/metainfo"
+       _ "github.com/anacrolix/envpprof"
+       "github.com/anacrolix/fuse"
+       fusefs "github.com/anacrolix/fuse/fs"
+       "github.com/anacrolix/missinggo/v2"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
 
-       "bazil.org/fuse"
-       fusefs "bazil.org/fuse/fs"
+       "github.com/anacrolix/torrent"
+       "github.com/anacrolix/torrent/internal/testutil"
+       "github.com/anacrolix/torrent/metainfo"
+       "github.com/anacrolix/torrent/storage"
 )
 
 func init() {
-       go http.ListenAndServe(":6061", nil)
+       log.SetFlags(log.Flags() | log.Lshortfile)
 }
 
 func TestTCPAddrString(t *testing.T) {
-       ta := &net.TCPAddr{
-               IP:   net.IPv4(127, 0, 0, 1),
-               Port: 3000,
-       }
-       s := ta.String()
-       l, err := net.Listen("tcp4", "localhost:3000")
+       l, err := net.Listen("tcp4", "localhost:0")
        if err != nil {
                t.Fatal(err)
        }
@@ -43,6 +41,11 @@ func TestTCPAddrString(t *testing.T) {
        }
        defer c.Close()
        ras := c.RemoteAddr().String()
+       ta := &net.TCPAddr{
+               IP:   net.IPv4(127, 0, 0, 1),
+               Port: missinggo.AddrPort(l.Addr()),
+       }
+       s := ta.String()
        if ras != s {
                t.FailNow()
        }
@@ -55,151 +58,183 @@ type testLayout struct {
        Metainfo  *metainfo.MetaInfo
 }
 
-func (me *testLayout) Destroy() error {
-       return os.RemoveAll(me.BaseDir)
+func (tl *testLayout) Destroy() error {
+       return os.RemoveAll(tl.BaseDir)
 }
 
-func newGreetingLayout() (tl testLayout, err error) {
-       tl.BaseDir, err = ioutil.TempDir("", "torrentfs")
-       if err != nil {
-               return
-       }
+func newGreetingLayout(t *testing.T) (tl testLayout, err error) {
+       tl.BaseDir = t.TempDir()
        tl.Completed = filepath.Join(tl.BaseDir, "completed")
-       os.Mkdir(tl.Completed, 0777)
+       os.Mkdir(tl.Completed, 0o777)
        tl.MountDir = filepath.Join(tl.BaseDir, "mnt")
-       os.Mkdir(tl.MountDir, 0777)
-       name := testutil.CreateDummyTorrentData(tl.Completed)
-       metaInfoBuf := &bytes.Buffer{}
-       testutil.CreateMetaInfo(name, metaInfoBuf)
-       tl.Metainfo, err = metainfo.Load(metaInfoBuf)
-       log.Printf("%x", tl.Metainfo.Info.Pieces)
+       os.Mkdir(tl.MountDir, 0o777)
+       testutil.CreateDummyTorrentData(tl.Completed)
+       tl.Metainfo = testutil.GreetingMetaInfo()
        return
 }
 
+// Unmount without first killing the FUSE connection while there are FUSE
+// operations blocked inside the filesystem code.
 func TestUnmountWedged(t *testing.T) {
-       layout, err := newGreetingLayout()
-       if err != nil {
-               t.Fatal(err)
-       }
+       layout, err := newGreetingLayout(t)
+       require.NoError(t, err)
        defer func() {
                err := layout.Destroy()
                if err != nil {
                        t.Log(err)
                }
        }()
-       client, err := torrent.NewClient(&torrent.Config{
-               DataDir:         filepath.Join(layout.BaseDir, "incomplete"),
-               DisableTrackers: true,
-               NoDHT:           true,
-       })
-       log.Printf("%+v", *layout.Metainfo)
-       client.AddTorrent(layout.Metainfo)
+       cfg := torrent.NewDefaultClientConfig()
+       cfg.DataDir = filepath.Join(layout.BaseDir, "incomplete")
+       cfg.DisableTrackers = true
+       cfg.NoDHT = true
+       cfg.DisableTCP = true
+       cfg.DisableUTP = true
+       client, err := torrent.NewClient(cfg)
+       require.NoError(t, err)
+       defer client.Close()
+       tt, err := client.AddTorrent(layout.Metainfo)
+       require.NoError(t, err)
        fs := New(client)
        fuseConn, err := fuse.Mount(layout.MountDir)
        if err != nil {
+               switch err.Error() {
+               case "cannot locate OSXFUSE":
+                       fallthrough
+               case "fusermount: exit status 1":
+                       t.Skip(err)
+               }
                t.Fatal(err)
        }
        go func() {
-               server := fusefs.Server{
-                       FS: fs,
+               server := fusefs.New(fuseConn, &fusefs.Config{
                        Debug: func(msg interface{}) {
-                               log.Print(msg)
+                               t.Log(msg)
                        },
-               }
-               server.Serve(fuseConn)
+               })
+               server.Serve(fs)
        }()
        <-fuseConn.Ready
        if err := fuseConn.MountError; err != nil {
-               log.Fatal(err)
+               t.Fatalf("mount error: %s", err)
        }
+       ctx, cancel := context.WithCancel(context.Background())
+       // Read the greeting file, though it will never be available. This should
+       // "wedge" FUSE, requiring the fs object to be forcibly destroyed. The
+       // read call will return with a FS error.
        go func() {
-               ioutil.ReadFile(filepath.Join(layout.MountDir, layout.Metainfo.Info.Name))
+               <-ctx.Done()
+               fs.mu.Lock()
+               fs.event.Broadcast()
+               fs.mu.Unlock()
        }()
-       time.Sleep(time.Second)
+       go func() {
+               defer cancel()
+               _, err := ioutil.ReadFile(filepath.Join(layout.MountDir, tt.Info().Name))
+               require.Error(t, err)
+       }()
+
+       // Wait until the read has blocked inside the filesystem code.
+       fs.mu.Lock()
+       for fs.blockedReads != 1 && ctx.Err() == nil {
+               fs.event.Wait()
+       }
+       fs.mu.Unlock()
+
        fs.Destroy()
-       time.Sleep(time.Second)
-       err = fuse.Unmount(layout.MountDir)
-       if err != nil {
-               log.Print(err)
+
+       for {
+               err = fuse.Unmount(layout.MountDir)
+               if err != nil {
+                       t.Logf("error unmounting: %s", err)
+                       time.Sleep(time.Millisecond)
+               } else {
+                       break
+               }
        }
+
        err = fuseConn.Close()
-       if err != nil {
-               t.Log(err)
-       }
+       assert.NoError(t, err)
 }
 
 func TestDownloadOnDemand(t *testing.T) {
-       layout, err := newGreetingLayout()
-       if err != nil {
-               t.Fatal(err)
-       }
-       seeder, err := torrent.NewClient(&torrent.Config{
-               DataDir:         layout.Completed,
-               DisableTrackers: true,
-               NoDHT:           true,
-       })
-       http.HandleFunc("/seeder", func(w http.ResponseWriter, req *http.Request) {
-               seeder.WriteStatus(w)
-       })
-       defer seeder.Stop()
-       _, err = seeder.AddMagnet(fmt.Sprintf("magnet:?xt=urn:btih:%x", layout.Metainfo.Info.Hash))
-       if err != nil {
-               t.Fatal(err)
-       }
-       leecher, err := torrent.NewClient(&torrent.Config{
-               DataDir:          filepath.Join(layout.BaseDir, "download"),
-               DownloadStrategy: torrent.NewResponsiveDownloadStrategy(0),
-               DisableTrackers:  true,
-               NoDHT:            true,
-       })
-       http.HandleFunc("/leecher", func(w http.ResponseWriter, req *http.Request) {
-               leecher.WriteStatus(w)
-       })
-       defer leecher.Stop()
-       leecher.AddTorrent(layout.Metainfo)
-       var ih torrent.InfoHash
-       util.CopyExact(ih[:], layout.Metainfo.Info.Hash)
-       leecher.AddPeers(ih, []torrent.Peer{func() torrent.Peer {
-               tcpAddr := seeder.ListenAddr().(*net.TCPAddr)
-               return torrent.Peer{
-                       IP:   tcpAddr.IP,
-                       Port: tcpAddr.Port,
-               }
-       }()})
-       fs := New(leecher)
-       mountDir := layout.MountDir
-       fuseConn, err := fuse.Mount(layout.MountDir)
-       if err != nil {
-               t.Fatal(err)
-       }
-       defer func() {
-               if err := fuse.Unmount(mountDir); err != nil {
-                       t.Fatal(err)
-               }
-       }()
+       layout, err := newGreetingLayout(t)
+       require.NoError(t, err)
+       defer layout.Destroy()
+       cfg := torrent.NewDefaultClientConfig()
+       cfg.DataDir = layout.Completed
+       cfg.DisableTrackers = true
+       cfg.NoDHT = true
+       cfg.Seed = true
+       cfg.ListenPort = 0
+       cfg.ListenHost = torrent.LoopbackListenHost
+       seeder, err := torrent.NewClient(cfg)
+       require.NoError(t, err)
+       defer seeder.Close()
+       defer testutil.ExportStatusWriter(seeder, "s", t)()
+       // Just to mix things up, the seeder starts with the data, but the leecher
+       // starts with the metainfo.
+       seederTorrent, err := seeder.AddMagnet(fmt.Sprintf("magnet:?xt=urn:btih:%s", layout.Metainfo.HashInfoBytes().HexString()))
+       require.NoError(t, err)
        go func() {
-               if err := fusefs.Serve(fuseConn, fs); err != nil {
-                       t.Fatal(err)
-               }
-               if err := fuseConn.Close(); err != nil {
-                       t.Fatal(err)
-               }
+               // Wait until we get the metainfo, then check for the data.
+               <-seederTorrent.GotInfo()
+               seederTorrent.VerifyData()
        }()
-       <-fuseConn.Ready
-       if fuseConn.MountError != nil {
-               t.Fatal(fuseConn.MountError)
-       }
-       go func() {
-               time.Sleep(10 * time.Second)
-               if err := fuse.Unmount(mountDir); err != nil {
-                       t.Log(err)
-               }
-       }()
-       content, err := ioutil.ReadFile(filepath.Join(mountDir, "greeting"))
-       if err != nil {
-               t.Fatal(err)
+       cfg = torrent.NewDefaultClientConfig()
+       cfg.DisableTrackers = true
+       cfg.NoDHT = true
+       cfg.DisableTCP = true
+       cfg.DefaultStorage = storage.NewMMap(filepath.Join(layout.BaseDir, "download"))
+       cfg.ListenHost = torrent.LoopbackListenHost
+       cfg.ListenPort = 0
+       leecher, err := torrent.NewClient(cfg)
+       require.NoError(t, err)
+       testutil.ExportStatusWriter(leecher, "l", t)()
+       defer leecher.Close()
+       leecherTorrent, err := leecher.AddTorrent(layout.Metainfo)
+       require.NoError(t, err)
+       leecherTorrent.AddClientPeer(seeder)
+       fs := New(leecher)
+       defer fs.Destroy()
+       root, _ := fs.Root()
+       node, _ := root.(fusefs.NodeStringLookuper).Lookup(context.Background(), "greeting")
+       var attr fuse.Attr
+       node.Attr(context.Background(), &attr)
+       size := attr.Size
+       data := make([]byte, size)
+       h, err := node.(fusefs.NodeOpener).Open(context.TODO(), nil, nil)
+       require.NoError(t, err)
+
+       // torrent.Reader.Read no longer tries to fill the entire read buffer, so this is a ReadFull for
+       // fusefs.
+       var n int
+       for n < len(data) {
+               resp := fuse.ReadResponse{Data: data[n:]}
+               err := h.(fusefs.HandleReader).Read(context.Background(), &fuse.ReadRequest{
+                       Size:   int(size) - n,
+                       Offset: int64(n),
+               }, &resp)
+               assert.NoError(t, err)
+               n += len(resp.Data)
        }
-       if string(content) != testutil.GreetingFileContents {
-               t.FailNow()
+
+       assert.EqualValues(t, testutil.GreetingFileContents, data)
+}
+
+func TestIsSubPath(t *testing.T) {
+       for _, case_ := range []struct {
+               parent, child string
+               is            bool
+       }{
+               {"", "", false},
+               {"", "/", true},
+               {"", "a", true},
+               {"a/b", "a/bc", false},
+               {"a/b", "a/b", false},
+               {"a/b", "a/b/c", true},
+               {"a/b", "a//b", false},
+       } {
+               assert.Equal(t, case_.is, isSubPath(case_.parent, case_.child))
        }
 }