Files
go-fuse/fs/simple_test.go

902 lines
21 KiB
Go

// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fs
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"syscall"
"testing"
"time"
"github.com/hanzoai/go-fuse/v2/fuse"
"github.com/hanzoai/go-fuse/v2/internal/testutil"
"github.com/hanzoai/go-fuse/v2/posixtest"
"github.com/kylelemons/godebug/pretty"
"golang.org/x/sys/unix"
)
type testCase struct {
*testing.T
dir string
origDir string
mntDir string
loopback InodeEmbedder
rawFS fuse.RawFileSystem
server *fuse.Server
}
// writeOrig writes a file into the backing directory of the loopback mount
func (tc *testCase) writeOrig(path, content string, mode os.FileMode) {
if err := os.WriteFile(filepath.Join(tc.origDir, path), []byte(content), mode); err != nil {
tc.Fatal(err)
}
}
func (tc *testCase) clean() {
if err := tc.server.Unmount(); err != nil {
tc.Fatal(err)
}
}
type testOptions struct {
entryCache bool
enableLocks bool
attrCache bool
suppressDebug bool
testDir string
ro bool
directMount bool // sets MountOptions.DirectMount
directMountStrict bool // sets MountOptions.DirectMountStrict
disableSplice bool // sets MountOptions.DisableSplice
idMappedMount bool // sets MountOptions.IDMappedMount
}
// newTestCase creates the directories `orig` and `mnt` inside a temporary
// directory and mounts a loopback filesystem, backed by `orig`, on `mnt`.
func newTestCase(t *testing.T, opts *testOptions) *testCase {
if opts == nil {
opts = &testOptions{}
}
if opts.testDir == "" {
opts.testDir = t.TempDir()
}
tc := &testCase{
dir: opts.testDir,
T: t,
}
tc.origDir = tc.dir + "/orig"
tc.mntDir = tc.dir + "/mnt"
if err := os.Mkdir(tc.origDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(tc.mntDir, 0755); err != nil {
t.Fatal(err)
}
var err error
tc.loopback, err = NewLoopbackRoot(tc.origDir)
if err != nil {
t.Fatalf("NewLoopback: %v", err)
}
oneSec := time.Second
attrDT := &oneSec
if !opts.attrCache {
attrDT = nil
}
entryDT := &oneSec
if !opts.entryCache {
entryDT = nil
}
tc.rawFS = NewNodeFS(tc.loopback, &Options{
EntryTimeout: entryDT,
AttrTimeout: attrDT,
Logger: log.New(os.Stderr, "", 0),
})
mOpts := &fuse.MountOptions{
DirectMount: opts.directMount,
DirectMountStrict: opts.directMountStrict,
EnableLocks: opts.enableLocks,
DisableSplice: opts.disableSplice,
IDMappedMount: opts.idMappedMount,
}
if !opts.suppressDebug {
mOpts.Debug = testutil.VerboseTest()
}
if opts.ro {
mOpts.Options = append(mOpts.Options, "ro")
}
if opts.idMappedMount {
mOpts.Options = append(mOpts.Options, "default_permissions")
}
tc.server, err = fuse.NewServer(tc.rawFS, tc.mntDir, mOpts)
if err != nil {
t.Fatal(err)
}
go tc.server.Serve()
if err := tc.server.WaitMount(); err != nil {
t.Fatal(err)
}
t.Cleanup(tc.clean)
return tc
}
func TestBasic(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
tc.writeOrig("file", "hello", 0644)
fn := tc.mntDir + "/file"
fi, err := os.Lstat(fn)
if err != nil {
t.Fatalf("Lstat: %v", err)
}
if fi.Size() != 5 {
t.Errorf("got size %d want 5", fi.Size())
}
stat := fuse.ToStatT(fi)
if got, want := uint32(stat.Mode), uint32(fuse.S_IFREG|0644); got != want {
t.Errorf("got mode %o, want %o", got, want)
}
if err := os.Remove(fn); err != nil {
t.Errorf("Remove: %v", err)
}
if fi, err := os.Lstat(fn); err == nil {
t.Errorf("Lstat after remove: got file %v", fi)
}
}
func TestExec(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
tc.writeOrig("test.sh", "#!/bin/sh\ntrue\n", 0755)
fn := tc.mntDir + "/test.sh"
if err := exec.Command("bash", "-c", fn).Run(); err != nil {
t.Fatalf("Run: %v", err)
}
}
func TestFileFdLeak(t *testing.T) {
tc := newTestCase(t, &testOptions{
suppressDebug: false,
attrCache: true,
entryCache: true,
})
posixtest.FdLeak(t, tc.mntDir)
tc.clean()
bridge := tc.rawFS.(*rawBridge)
tc = nil
// posixtest.FdLeak also uses 15 as a limit.
if got, want := len(bridge.files), 15; got > want {
t.Errorf("found %d used file handles, should be <= %d", got, want)
}
}
func TestNotifyEntry(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
orig := tc.origDir + "/file"
fn := tc.mntDir + "/file"
tc.writeOrig("file", "hello", 0644)
st := syscall.Stat_t{}
if err := syscall.Lstat(fn, &st); err != nil {
t.Fatalf("Lstat before: %v", err)
}
if err := os.Remove(orig); err != nil {
t.Fatalf("Remove: %v", err)
}
after := syscall.Stat_t{}
if err := syscall.Lstat(fn, &after); err != nil {
t.Fatalf("Lstat after: %v", err)
} else if !reflect.DeepEqual(st, after) {
t.Fatalf("got after %#v, want %#v", after, st)
}
if errno := tc.loopback.EmbeddedInode().NotifyEntry("file"); errno != 0 {
t.Errorf("notify failed: %v", errno)
}
if err := syscall.Lstat(fn, &after); err != syscall.ENOENT {
t.Fatalf("Lstat after: got %v, want ENOENT", err)
}
}
func TestReadDirStress(t *testing.T) {
tc := newTestCase(t, &testOptions{suppressDebug: true, attrCache: true, entryCache: true})
// Create 110 entries
for i := 0; i < 110; i++ {
name := fmt.Sprintf("file%036x", i)
if err := os.WriteFile(filepath.Join(tc.mntDir, name), []byte("hello"), 0644); err != nil {
t.Fatalf("WriteFile %q: %v", name, err)
}
}
var wg sync.WaitGroup
stress := func(gr int) {
defer wg.Done()
for i := 1; i < 100; i++ {
f, err := os.Open(tc.mntDir)
if err != nil {
t.Error(err)
return
}
_, err = f.Readdirnames(-1)
f.Close()
if err != nil {
t.Errorf("goroutine %d iteration %d: %v", gr, i, err)
return
}
}
}
n := 3
for i := 1; i <= n; i++ {
wg.Add(1)
go stress(i)
}
wg.Wait()
}
func TestStatFs(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: true, entryCache: true})
var before, after, mnt syscall.Statfs_t
if err := syscall.Statfs(tc.origDir, &before); err != nil {
t.Fatal("statfs before", err)
}
if err := syscall.Statfs(tc.mntDir, &mnt); err != nil {
t.Fatal("statfs mnt", err)
}
if err := syscall.Statfs(tc.origDir, &after); err != nil {
t.Fatal("statfs before", err)
}
var beforeFuse, afterFuse, mntFuse fuse.StatfsOut
mntFuse.FromStatfsT(&mnt)
beforeFuse.FromStatfsT(&before)
afterFuse.FromStatfsT(&after)
if !reflect.DeepEqual(beforeFuse, afterFuse) {
// This test is racy. If an external process consumes
// space while this runs, we may see spurious
// differences between the two statfs() calls.
t.Skipf("FS changed during test run, %v != %v", beforeFuse, afterFuse)
}
if !reflect.DeepEqual(mntFuse, beforeFuse) {
t.Errorf("Got %#v, want %#v", mntFuse, beforeFuse)
}
}
func TestGetAttrParallel(t *testing.T) {
// We grab a file-handle to provide to the API so rename+fstat
// can be handled correctly. Here, test that closing and
// (f)stat in parallel don't lead to fstat on closed files.
// We can only test that if we switch off caching
tc := newTestCase(t, &testOptions{suppressDebug: true})
N := 100
var fds []int
var fns []string
for i := 0; i < N; i++ {
fn := fmt.Sprintf("file%d", i)
tc.writeOrig(fn, "ello", 0644)
fn = filepath.Join(tc.mntDir, fn)
fns = append(fns, fn)
fd, err := syscall.Open(fn, syscall.O_RDONLY, 0)
if err != nil {
t.Fatalf("Open %d: %v", i, err)
}
fds = append(fds, fd)
}
var wg sync.WaitGroup
wg.Add(2 * N)
for i := 0; i < N; i++ {
go func(i int) {
if err := syscall.Close(fds[i]); err != nil {
t.Errorf("close %d: %v", i, err)
}
wg.Done()
}(i)
go func(i int) {
var st syscall.Stat_t
if err := syscall.Lstat(fns[i], &st); err != nil {
t.Errorf("lstat %d: %v", i, err)
}
wg.Done()
}(i)
}
wg.Wait()
}
func TestMknod(t *testing.T) {
tc := newTestCase(t, &testOptions{})
modes := map[string]uint32{
"regular": syscall.S_IFREG,
"socket": syscall.S_IFSOCK,
"fifo": syscall.S_IFIFO,
}
for nm, mode := range modes {
t.Run(nm, func(t *testing.T) {
p := filepath.Join(tc.mntDir, nm)
err := syscall.Mknod(p, mode|0755, (8<<8)|0)
if err != nil {
t.Fatalf("mknod(%s): %v", nm, err)
}
var st syscall.Stat_t
if err := syscall.Stat(p, &st); err != nil {
got := st.Mode &^ 07777
if want := uint(mode); want != uint(got) {
t.Fatalf("stat(%s): got %o want %o", nm, got, want)
}
}
// We could test if the files can be
// read/written but: The kernel handles FIFOs
// without talking to FUSE at all. Presumably,
// this also holds for sockets. Regular files
// are tested extensively elsewhere.
})
}
}
func TestMknodNotSupported(t *testing.T) {
mountPoint := t.TempDir()
server, err := Mount(mountPoint, &Inode{}, nil)
if err != nil {
t.Fatalf("cannot mount: %v", err)
}
defer server.Unmount()
name := filepath.Join(mountPoint, "foo")
if got, want := syscall.Mknod(name, syscall.S_IFREG|0755, (8<<8)|0), syscall.ENOTSUP; got != want {
t.Fatalf("mknod: got %v, want %v", got, want)
}
}
func TestPosix(t *testing.T) {
noisy := map[string]bool{
"ParallelFileOpen": true,
"ReadDir": true,
}
for nm, fn := range posixtest.All {
t.Run(nm, func(t *testing.T) {
tc := newTestCase(t, &testOptions{
suppressDebug: noisy[nm],
attrCache: true,
entryCache: true,
enableLocks: true,
})
fn(t, tc.mntDir)
})
}
}
func TestReadDisableSplice(t *testing.T) {
tc := newTestCase(t, &testOptions{
disableSplice: true,
})
posixtest.FileBasic(t, tc.mntDir)
}
func TestOpenDirectIO(t *testing.T) {
// Apparently, tmpfs does not allow O_DIRECT, so try to create
// a test temp directory in /var/tmp.
ext4Dir, err := os.MkdirTemp("/var/tmp", "go-fuse.TestOpenDirectIO")
if err != nil {
t.Fatalf("MkdirAll: %v", err)
}
t.Cleanup(func() { os.RemoveAll(ext4Dir) })
posixtest.DirectIO(t, ext4Dir)
if t.Failed() {
t.Skip("DirectIO failed on underlying FS")
}
opts := testOptions{
testDir: ext4Dir,
attrCache: true,
entryCache: true,
}
tc := newTestCase(t, &opts)
posixtest.DirectIO(t, tc.mntDir)
}
// TestFsstress is loosely modeled after xfstest's fsstress. It performs rapid
// parallel removes / creates / readdirs. Coupled with inode reuse, this test
// used to deadlock go-fuse quite quickly.
//
// Note: Run as
//
// TMPDIR=/var/tmp go test -run TestFsstress
//
// to make sure the backing filesystem is ext4. /tmp is tmpfs on modern Linux
// distributions, and tmpfs does not reuse inode numbers, hiding the problem.
func TestFsstress(t *testing.T) {
tc := newTestCase(t, &testOptions{suppressDebug: true, attrCache: true, entryCache: true})
{
old := runtime.GOMAXPROCS(100)
defer runtime.GOMAXPROCS(old)
}
const concurrency = 10
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
// operations taking 1 path argument
ops1 := map[string]func(string) error{
"mkdir": func(p string) error { return syscall.Mkdir(p, 0700) },
"rmdir": func(p string) error { return syscall.Rmdir(p) },
"mknod_reg": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFREG, 0) },
"remove": os.Remove,
"unlink": syscall.Unlink,
"mknod_sock": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFSOCK, 0) },
"mknod_fifo": func(p string) error { return syscall.Mknod(p, 0700|syscall.S_IFIFO, 0) },
"mkfifo": func(p string) error { return syscall.Mkfifo(p, 0700) },
"symlink": func(p string) error { return syscall.Symlink("foo", p) },
"creat": func(p string) error {
fd, err := syscall.Open(p, syscall.O_CREAT|syscall.O_EXCL, 0700)
if err == nil {
syscall.Close(fd)
}
return err
},
}
// operations taking 2 path arguments
ops2 := map[string]func(string, string) error{
"rename": syscall.Rename,
"link": syscall.Link,
}
type opStats struct {
ok *int64
fail *int64
hung *int64
}
stats := make(map[string]opStats)
// pathN() returns something like /var/tmp/TestFsstress/TestFsstress.4
pathN := func(n int) string {
return fmt.Sprintf("%s/%s.%d", tc.mntDir, t.Name(), n)
}
opLoop := func(k string, n int) {
defer wg.Done()
op := ops1[k]
for {
p := pathN(1)
atomic.AddInt64(stats[k].hung, 1)
err := op(p)
atomic.AddInt64(stats[k].hung, -1)
if err != nil {
atomic.AddInt64(stats[k].fail, 1)
} else {
atomic.AddInt64(stats[k].ok, 1)
}
select {
case <-ctx.Done():
return
default:
}
}
}
op2Loop := func(k string, n int) {
defer wg.Done()
op := ops2[k]
n2 := (n + 1) % concurrency
for {
p1 := pathN(n)
p2 := pathN(n2)
atomic.AddInt64(stats[k].hung, 1)
err := op(p1, p2)
atomic.AddInt64(stats[k].hung, -1)
if err != nil {
atomic.AddInt64(stats[k].fail, 1)
} else {
atomic.AddInt64(stats[k].ok, 1)
}
select {
case <-ctx.Done():
return
default:
}
}
}
readdirLoop := func(k string) {
defer wg.Done()
for {
atomic.AddInt64(stats[k].hung, 1)
f, err := os.Open(tc.mntDir)
if err != nil {
panic(err)
}
_, err = f.Readdir(0)
if err != nil {
atomic.AddInt64(stats[k].fail, 1)
} else {
atomic.AddInt64(stats[k].ok, 1)
}
f.Close()
atomic.AddInt64(stats[k].hung, -1)
select {
case <-ctx.Done():
return
default:
}
}
}
// prepare stats map
var allOps []string
for k := range ops1 {
allOps = append(allOps, k)
}
for k := range ops2 {
allOps = append(allOps, k)
}
allOps = append(allOps, "readdir")
for _, k := range allOps {
var i1, i2, i3 int64
stats[k] = opStats{ok: &i1, fail: &i2, hung: &i3}
}
// spawn worker goroutines
for i := 0; i < concurrency; i++ {
for k := range ops1 {
wg.Add(1)
go opLoop(k, i)
}
for k := range ops2 {
wg.Add(1)
go op2Loop(k, i)
}
}
{
k := "readdir"
wg.Add(1)
go readdirLoop(k)
}
// spawn ls loop
//
// An external "ls" loop has a destructive effect that I am unable to
// reproduce through in-process operations.
if strings.ContainsAny(tc.mntDir, "'\\") {
// But let's not enable shell injection.
log.Panicf("shell injection attempt? mntDir=%q", tc.mntDir)
}
// --color=always enables xattr lookups for extra stress
cmd := exec.Command("bash", "-c", "while true ; do ls -l --color=always '"+tc.mntDir+"'; done")
err := cmd.Start()
if err != nil {
t.Fatal(err)
}
wg.Add(1)
go func() {
cmd.Wait()
wg.Done()
}()
defer cmd.Process.Kill()
// Run the test for 1 second. If it deadlocks, it usually does within 20ms.
time.Sleep(1 * time.Second)
cancel()
cmd.Process.Kill()
// waitTimeout waits for the waitgroup for the specified max timeout.
// Returns true if waiting timed out.
waitTimeout := func(wg *sync.WaitGroup, timeout time.Duration) bool {
c := make(chan struct{})
go func() {
defer close(c)
wg.Wait()
}()
select {
case <-c:
return false // completed normally
case <-time.After(timeout):
return true // timed out
}
}
if waitTimeout(&wg, time.Second) {
t.Errorf("timeout waiting for goroutines to exit (deadlocked?)")
}
// Print operation statistics
var keys []string
for k := range stats {
keys = append(keys, k)
}
sort.Strings(keys)
t.Logf("Operation statistics:")
for _, k := range keys {
v := stats[k]
t.Logf("%10s: %5d ok, %6d fail, %2d hung", k, *v.ok, *v.fail, *v.hung)
}
}
// TestStaleHardlinks creates a lot of hard links and deletes them again
// behind the back of the loopback fs. Then opens the original file.
//
// Fails at the moment. Core of the problem:
//
// 18:41:50.796468 rx 136: LOOKUP n1 ["link0"] 6b
// 18:41:50.796489 tx 136: OK, {n2 g1 tE=0s tA=0s {M0100600 SZ=0 L=1 1026:1026 B0*4096 i0:269663 A 1616348510.793212 M 1616348510.793212 C 1616348510.795212}}
// 18:41:50.796535 rx 138: OPEN n2 {O_RDONLY,0x8000}
// 18:41:50.796557 tx 138: 2=no such file or directory, {Fh 0 }
func TestStaleHardlinks(t *testing.T) {
// Disable all caches we can disable
tc := newTestCase(t, &testOptions{attrCache: false, entryCache: false})
// gvfsd-trash sets an inotify watch on mntDir and stat()s every file that is
// created, racing with the test logic ( https://github.com/hanwen/go-fuse/issues/478 ).
// Use a subdir to prevent that.
if err := os.Mkdir(tc.mntDir+"/x", 0755); err != nil {
t.Fatal(err)
}
// "link0" is original file
link0 := tc.mntDir + "/x/link0"
if fd, err := unix.Open(link0, unix.O_CREAT, 0600); err != nil {
t.Fatal(err)
} else {
syscall.Close(fd)
}
// Create hardlinks via mntDir
t.Logf("create link1...20, pid=%d", os.Getpid())
for i := 1; i < 20; i++ {
linki := fmt.Sprintf(tc.mntDir+"/x/link%d", i)
if err := syscall.Link(link0, linki); err != nil {
t.Fatal(err)
}
}
// Delete hardlinks via origDir (behind loopback fs's back)
t.Log("delete link1...20 behind loopback's back")
for i := 1; i < 20; i++ {
linki := fmt.Sprintf(tc.origDir+"/x/link%d", i)
if err := syscall.Unlink(linki); err != nil {
t.Fatal(err)
}
}
// Try to open link0 via mntDir
t.Log("open link0")
fd, err := syscall.Open(link0, syscall.O_RDONLY, 0)
if err != nil {
t.Error(err)
} else {
syscall.Close(fd)
}
}
func init() {
syscall.Umask(0)
}
func testMountDir(dir string) error {
opts := &Options{}
opts.Debug = testutil.VerboseTest()
server, err := Mount(dir, &Inode{}, opts)
if err != nil {
return err
}
server.Unmount()
server.Wait()
return nil
}
func TestParallelMount(t *testing.T) {
before := runtime.GOMAXPROCS(1)
defer runtime.GOMAXPROCS(before)
// Per default, only 1000 FUSE mounts are allowed, then you get
// > /usr/bin/fusermount3: too many FUSE filesystems mounted; mount_max=N can be set in /etc/fuse.conf
// Let's stay well below 1000.
N := 100
todo := make(chan string, N)
result := make(chan error, N)
for i := 0; i < N; i++ {
todo <- t.TempDir()
}
close(todo)
P := 2
for i := 0; i < P; i++ {
go func() {
for d := range todo {
result <- testMountDir(d)
}
}()
}
for i := 0; i < N; i++ {
e := <-result
if e != nil {
t.Error(e)
}
}
}
type handleLessCreateNode struct {
Inode
}
var _ = (NodeCreater)((*handleLessCreateNode)(nil))
func (n *handleLessCreateNode) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (node *Inode, fh FileHandle, fuseFlags uint32, errno syscall.Errno) {
f := &MemRegularFile{
Attr: fuse.Attr{
Mode: mode,
},
}
ch := n.NewPersistentInode(ctx, f, StableAttr{Mode: fuse.S_IFREG})
n.AddChild(name, ch, true)
return ch, nil, fuse.FOPEN_KEEP_CACHE, 0
}
func TestHandleLessCreate(t *testing.T) {
hlcn := &handleLessCreateNode{}
dir, _ := testMount(t, hlcn, nil)
posixtest.FileBasic(t, dir)
}
func lstatxPath(p string) (*unix.Statx_t, error) {
var r unix.Statx_t
err := unix.Statx(unix.AT_FDCWD, p, unix.AT_SYMLINK_NOFOLLOW,
(unix.STATX_BASIC_STATS | unix.STATX_BTIME |
unix.STATX_MNT_ID), // unix.STATX_DIOALIGN
&r)
return &r, err
}
func clearStatx(st *unix.Statx_t, mask uint32) {
st.Mask = mask
if mask&(unix.STATX_TYPE|unix.STATX_MODE) == 0 {
st.Mode = 0
}
if mask&(unix.STATX_NLINK) == 0 {
st.Nlink = 0
}
if mask&(unix.STATX_INO) == 0 {
st.Ino = 0
}
if mask&(unix.STATX_ATIME) == 0 {
st.Atime = unix.StatxTimestamp{}
}
if mask&(unix.STATX_BTIME) == 0 {
st.Btime = unix.StatxTimestamp{}
}
if mask&(unix.STATX_CTIME) == 0 {
st.Ctime = unix.StatxTimestamp{}
}
if mask&(unix.STATX_MTIME) == 0 {
st.Mtime = unix.StatxTimestamp{}
}
st.Dev_minor = 0
st.Dev_major = 0
st.Mnt_id = 0
st.Attributes_mask = 0
}
func TestStatx(t *testing.T) {
tc := newTestCase(t, &testOptions{attrCache: false, entryCache: false})
if err := os.WriteFile(tc.origDir+"/file", []byte("blabla"), 0644); err != nil {
t.Fatal(err)
}
oFile := tc.origDir + "/file"
want, err := lstatxPath(oFile)
if err != nil {
t.Fatal(err)
}
mFile := tc.mntDir + "/file"
got, err := lstatxPath(mFile)
if err != nil {
t.Fatal(err)
}
mask := got.Mask & want.Mask
clearStatx(got, mask)
clearStatx(want, mask)
if diff := pretty.Compare(got, want); diff != "" {
t.Errorf("got, want: %s", diff)
}
// the following works, but does not set Fh in StatxIn
oFD, err := os.Open(oFile)
if err != nil {
t.Fatal(err)
}
defer oFD.Close()
mFD, err := os.Open(mFile)
if err != nil {
t.Fatal(err)
}
defer mFD.Close()
var osx, msx unix.Statx_t
if err := unix.Statx(int(oFD.Fd()), "", unix.AT_EMPTY_PATH,
(unix.STATX_BASIC_STATS | unix.STATX_BTIME |
unix.STATX_MNT_ID), // unix.STATX_DIOALIGN
&osx); err != nil {
t.Fatal(err)
}
if err := unix.Statx(int(mFD.Fd()), "", unix.AT_EMPTY_PATH,
(unix.STATX_BASIC_STATS | unix.STATX_BTIME |
unix.STATX_MNT_ID), // unix.STATX_DIOALIGN
&msx); err != nil {
t.Fatal(err)
}
mask = msx.Mask & osx.Mask
clearStatx(&msx, mask)
clearStatx(&osx, mask)
if diff := pretty.Compare(msx, osx); diff != "" {
t.Errorf("got, want: %s", diff)
}
}
// A dangling symlink should not cause lchown to fail with ENOENT.
// This is used in apt-get install.
func TestLChown(t *testing.T) {
tc := newTestCase(t, &testOptions{})
if err := os.Symlink("doesnotexist", tc.origDir+"/link"); err != nil {
t.Fatal(err)
}
if err := os.Lchown(tc.mntDir+"/link", os.Getuid(), os.Getgid()); err != nil {
t.Fatal(err)
}
}