diff --git a/fusetesting/parallel.go b/fusetesting/parallel.go index 775c983..4bb19fb 100644 --- a/fusetesting/parallel.go +++ b/fusetesting/parallel.go @@ -365,3 +365,71 @@ func RunSymlinkInParallelTest( AssertEq(nil, err) } } + +// Run an ogletest test that checks expectations for parallel calls to +// link(2). +func RunHardlinkInParallelTest( + ctx context.Context, + dir string) { + // Ensure that we get parallelism for this test. + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(runtime.NumCPU())) + + // Create a file. + originalFile := path.Join(dir, "original_file") + const contents = "Hello\x00world" + + err := ioutil.WriteFile(originalFile, []byte(contents), 0444) + AssertEq(nil, err) + + // Try for awhile to see if anything breaks. + const duration = 500 * time.Millisecond + startTime := time.Now() + for time.Since(startTime) < duration { + filename := path.Join(dir, "foo") + + // Set up a function that creates the symlink, ignoring EEXIST errors. + worker := func(id byte) (err error) { + err = os.Link(originalFile, filename) + + if os.IsExist(err) { + err = nil + } + + if err != nil { + err = fmt.Errorf("Worker %d: Link: %v", id, err) + return + } + + return + } + + // Run several workers in parallel. + const numWorkers = 16 + b := syncutil.NewBundle(ctx) + for i := 0; i < numWorkers; i++ { + id := byte(i) + b.Add(func(ctx context.Context) (err error) { + err = worker(id) + return + }) + } + + err := b.Join() + AssertEq(nil, err) + + // The symlink should have been created, once. + entries, err := ReadDirPicky(dir) + AssertEq(nil, err) + AssertEq(2, len(entries)) + AssertEq("foo", entries[0].Name()) + AssertEq("original_file", entries[1].Name()) + + // Remove the link. + err = os.Remove(filename) + AssertEq(nil, err) + } + + // Clean up the original file at the end. + err = os.Remove(originalFile) + AssertEq(nil, err) +} diff --git a/samples/memfs/memfs.go b/samples/memfs/memfs.go index bf7c570..ae5c337 100644 --- a/samples/memfs/memfs.go +++ b/samples/memfs/memfs.go @@ -424,6 +424,46 @@ func (fs *memFS) CreateSymlink( return } +func (fs *memFS) CreateLink( + ctx context.Context, + op *fuseops.CreateLinkOp) (err error) { + fs.mu.Lock() + defer fs.mu.Unlock() + + // Grab the parent, which we will update shortly. + parent := fs.getInodeOrDie(op.Parent) + + // Ensure that the name doesn't already exist, so we don't wind up with a + // duplicate. + _, _, exists := parent.LookUpChild(op.Name) + if exists { + err = fuse.EEXIST + return + } + + // Get the target inode to be linked + target := fs.getInodeOrDie(op.Target) + + // Update the attributes + now := time.Now() + target.attrs.Nlink++ + target.attrs.Ctime = now + + // Add an entry in the parent. + parent.AddChild(op.Target, op.Name, fuseutil.DT_File) + + // Return the response. + op.Entry.Child = op.Target + op.Entry.Attributes = target.attrs + + // We don't spontaneously mutate, so the kernel can cache as long as it wants + // (since it also handles invalidation). + op.Entry.AttributesExpiration = time.Now().Add(365 * 24 * time.Hour) + op.Entry.EntryExpiration = op.Entry.EntryExpiration + + return +} + func (fs *memFS) Rename( ctx context.Context, op *fuseops.RenameOp) (err error) { diff --git a/samples/memfs/memfs_test.go b/samples/memfs/memfs_test.go index dcdad01..6e78a1b 100644 --- a/samples/memfs/memfs_test.go +++ b/samples/memfs/memfs_test.go @@ -21,6 +21,7 @@ import ( "os" "os/user" "path" + "reflect" "runtime" "strconv" "syscall" @@ -1088,26 +1089,6 @@ func (t *MemFSTest) ReadDirWhileModifying() { ExpectTrue(namesSeen["qux"]) } -func (t *MemFSTest) HardLinks() { - var err error - - // Create a file and a directory. - fileName := path.Join(t.Dir, "foo") - err = ioutil.WriteFile(fileName, []byte{}, 0400) - AssertEq(nil, err) - - dirName := path.Join(t.Dir, "bar") - err = os.Mkdir(dirName, 0700) - AssertEq(nil, err) - - // Attempt to link each. Neither should work, but for different reasons. - err = os.Link(fileName, path.Join(t.Dir, "baz")) - ExpectThat(err, Error(HasSubstr("not implemented"))) - - err = os.Link(dirName, path.Join(t.Dir, "baz")) - ExpectThat(err, Error(HasSubstr("not permitted"))) -} - func (t *MemFSTest) CreateSymlink() { var fi os.FileInfo var err error @@ -1225,6 +1206,202 @@ func (t *MemFSTest) DeleteSymlink() { ExpectThat(entries, ElementsAre()) } +func (t *MemFSTest) CreateHardlink() { + var fi os.FileInfo + var err error + + // Create a file. + fileName := path.Join(t.Dir, "regular_file") + const contents = "Hello\x00world" + + err = ioutil.WriteFile(fileName, []byte(contents), 0444) + AssertEq(nil, err) + + // Clean up the file at the end. + defer func() { + err := os.Remove(fileName) + AssertEq(nil, err) + }() + + // Create a link to the file. + linkName := path.Join(t.Dir, "foo") + err = os.Link(fileName, linkName) + AssertEq(nil, err) + + // Clean up the file at the end. + defer func() { + err := os.Remove(linkName) + AssertEq(nil, err) + }() + + // Stat the link. + fi, err = os.Lstat(linkName) + AssertEq(nil, err) + + ExpectEq("foo", fi.Name()) + ExpectEq(0444, fi.Mode()) + + // Read the parent directory. + entries, err := fusetesting.ReadDirPicky(t.Dir) + AssertEq(nil, err) + AssertEq(2, len(entries)) + + fi = entries[0] + ExpectEq("foo", fi.Name()) + ExpectEq(0444, fi.Mode()) + + fi = entries[1] + ExpectEq("regular_file", fi.Name()) + ExpectEq(0444, fi.Mode()) +} + +func (t *MemFSTest) CreateHardlink_AlreadyExists() { + var err error + + // Create a file and a directory. + fileName := path.Join(t.Dir, "foo") + err = ioutil.WriteFile(fileName, []byte{}, 0400) + AssertEq(nil, err) + + dirName := path.Join(t.Dir, "bar") + err = os.Mkdir(dirName, 0700) + AssertEq(nil, err) + + // Create an existing symlink. + symlinkName := path.Join(t.Dir, "baz") + err = os.Symlink("blah", symlinkName) + AssertEq(nil, err) + + // Create another link to the file. + hardlinkName := path.Join(t.Dir, "qux") + err = os.Link(fileName, hardlinkName) + AssertEq(nil, err) + + // Symlinking on top of any of them should fail. + names := []string{ + fileName, + dirName, + symlinkName, + hardlinkName, + } + + for _, n := range names { + err = os.Link(fileName, n) + ExpectThat(err, Error(HasSubstr("exists"))) + } +} + +func (t *MemFSTest) DeleteHardlink() { + var fi os.FileInfo + var err error + + // Create a file. + fileName := path.Join(t.Dir, "regular_file") + const contents = "Hello\x00world" + + err = ioutil.WriteFile(fileName, []byte(contents), 0444) + AssertEq(nil, err) + + // Step #1: We will create and remove a link and verify that + // after removal everything is as expected. + + // Create a link to the file. + linkName := path.Join(t.Dir, "foo") + err = os.Link(fileName, linkName) + AssertEq(nil, err) + + // Remove the link. + err = os.Remove(linkName) + AssertEq(nil, err) + + // Stat the link. + fi, err = os.Lstat(linkName) + AssertEq(nil, fi) + ExpectThat(err, Error(HasSubstr("no such file"))) + + // Read the parent directory. + entries, err := fusetesting.ReadDirPicky(t.Dir) + AssertEq(nil, err) + AssertEq(1, len(entries)) + + fi = entries[0] + ExpectEq("regular_file", fi.Name()) + ExpectEq(0444, fi.Mode()) + + // Step #2: We will create a link and remove the original file subsequently + // and verify that after removal everything is as expected. + + // Create a link to the file. + linkName = path.Join(t.Dir, "bar") + err = os.Link(fileName, linkName) + AssertEq(nil, err) + + // Remove the original file. + err = os.Remove(fileName) + AssertEq(nil, err) + + // Stat the link. + fi, err = os.Lstat(linkName) + AssertEq(nil, err) + ExpectEq("bar", fi.Name()) + ExpectEq(0444, fi.Mode()) + + // Stat the original file. + fi, err = os.Lstat(fileName) + AssertEq(nil, fi) + ExpectThat(err, Error(HasSubstr("no such file"))) + + // Read the parent directory. + entries, err = fusetesting.ReadDirPicky(t.Dir) + AssertEq(nil, err) + AssertEq(1, len(entries)) + + fi = entries[0] + ExpectEq("bar", fi.Name()) + ExpectEq(0444, fi.Mode()) + + // Cleanup. + err = os.Remove(linkName) + AssertEq(nil, err) +} + +func (t *MemFSTest) ReadHardlink() { + var err error + + // Create a file. + fileName := path.Join(t.Dir, "regular_file") + const contents = "Hello\x00world" + + err = ioutil.WriteFile(fileName, []byte(contents), 0444) + AssertEq(nil, err) + + // Clean up the file at the end. + defer func() { + err := os.Remove(fileName) + AssertEq(nil, err) + }() + + // Create a link to the file. + linkName := path.Join(t.Dir, "foo") + err = os.Link(fileName, linkName) + AssertEq(nil, err) + + // Clean up the file at the end. + defer func() { + err := os.Remove(linkName) + AssertEq(nil, err) + }() + + // Read files. + original, err := ioutil.ReadFile(fileName) + AssertEq(nil, err) + linked, err := ioutil.ReadFile(linkName) + AssertEq(nil, err) + + // Check if the bytes are the same. + AssertEq(true, reflect.DeepEqual(original, linked)) +} + func (t *MemFSTest) CreateInParallel_NoTruncate() { fusetesting.RunCreateInParallelTest_NoTruncate(t.Ctx, t.Dir) } @@ -1245,6 +1422,10 @@ func (t *MemFSTest) SymlinkInParallel() { fusetesting.RunSymlinkInParallelTest(t.Ctx, t.Dir) } +func (t *MemFSTest) HardlinkInParallel() { + fusetesting.RunHardlinkInParallelTest(t.Ctx, t.Dir) +} + func (t *MemFSTest) RenameWithinDir_File() { var err error diff --git a/samples/memfs/posix_test.go b/samples/memfs/posix_test.go index 50e947c..859f839 100644 --- a/samples/memfs/posix_test.go +++ b/samples/memfs/posix_test.go @@ -445,3 +445,7 @@ func (t *PosixTest) MkdirInParallel() { func (t *PosixTest) SymlinkInParallel() { fusetesting.RunSymlinkInParallelTest(t.ctx, t.dir) } + +func (t *PosixTest) HardlinkInParallel() { + fusetesting.RunHardlinkInParallelTest(t.ctx, t.dir) +}