github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/sysfs/stat_test.go (about)

     1  package sysfs
     2  
     3  import (
     4  	"os"
     5  	"path"
     6  	"runtime"
     7  	"testing"
     8  	"time"
     9  
    10  	experimentalsys "github.com/bananabytelabs/wazero/experimental/sys"
    11  	"github.com/bananabytelabs/wazero/internal/testing/require"
    12  	"github.com/bananabytelabs/wazero/sys"
    13  )
    14  
    15  func TestStat(t *testing.T) {
    16  	tmpDir := t.TempDir()
    17  
    18  	_, errno := stat(path.Join(tmpDir, "cat"))
    19  	require.EqualErrno(t, experimentalsys.ENOENT, errno)
    20  	_, errno = stat(path.Join(tmpDir, "sub/cat"))
    21  	require.EqualErrno(t, experimentalsys.ENOENT, errno)
    22  
    23  	var st sys.Stat_t
    24  
    25  	t.Run("empty dir", func(t *testing.T) {
    26  		st, errno = stat(tmpDir)
    27  		require.EqualErrno(t, 0, errno)
    28  
    29  		require.True(t, st.Mode.IsDir())
    30  		require.NotEqual(t, uint64(0), st.Ino)
    31  
    32  		// We expect one link: the directory itself
    33  		expectedNlink := uint64(1)
    34  		if dirNlinkIncludesDot {
    35  			expectedNlink++
    36  		}
    37  		require.Equal(t, expectedNlink, st.Nlink, runtime.GOOS)
    38  	})
    39  
    40  	subdir := path.Join(tmpDir, "sub")
    41  	var stSubdir sys.Stat_t
    42  	t.Run("subdir", func(t *testing.T) {
    43  		require.NoError(t, os.Mkdir(subdir, 0o500))
    44  
    45  		stSubdir, errno = stat(subdir)
    46  		require.EqualErrno(t, 0, errno)
    47  
    48  		require.True(t, stSubdir.Mode.IsDir())
    49  		require.NotEqual(t, uint64(0), st.Ino)
    50  	})
    51  
    52  	t.Run("not empty dir", func(t *testing.T) {
    53  		st, errno = stat(tmpDir)
    54  		require.EqualErrno(t, 0, errno)
    55  
    56  		// We expect two links: the directory itself and the subdir
    57  		expectedNlink := uint64(2)
    58  		if dirNlinkIncludesDot {
    59  			expectedNlink++
    60  		} else if runtime.GOOS == "windows" {
    61  			expectedNlink = 1 // directory count is not returned.
    62  		}
    63  		require.Equal(t, expectedNlink, st.Nlink, runtime.GOOS)
    64  	})
    65  
    66  	// TODO: Investigate why Nlink increases on BSD when a file is added, but
    67  	// not Linux.
    68  
    69  	file := path.Join(tmpDir, "file")
    70  	var stFile sys.Stat_t
    71  
    72  	t.Run("file", func(t *testing.T) {
    73  		require.NoError(t, os.WriteFile(file, nil, 0o400))
    74  
    75  		stFile, errno = stat(file)
    76  		require.EqualErrno(t, 0, errno)
    77  
    78  		require.False(t, stFile.Mode.IsDir())
    79  		require.NotEqual(t, uint64(0), st.Ino)
    80  	})
    81  
    82  	t.Run("link to file", func(t *testing.T) {
    83  		link := path.Join(tmpDir, "file-link")
    84  		require.NoError(t, os.Symlink(file, link))
    85  
    86  		stLink, errno := stat(link)
    87  		require.EqualErrno(t, 0, errno)
    88  
    89  		require.Equal(t, stFile, stLink) // resolves to the file
    90  	})
    91  
    92  	t.Run("link to dir", func(t *testing.T) {
    93  		link := path.Join(tmpDir, "dir-link")
    94  		require.NoError(t, os.Symlink(subdir, link))
    95  
    96  		stLink, errno := stat(link)
    97  		require.EqualErrno(t, 0, errno)
    98  
    99  		require.Equal(t, stSubdir, stLink) // resolves to the dir
   100  	})
   101  }
   102  
   103  func TestStatFile(t *testing.T) {
   104  	tmpDir := t.TempDir()
   105  
   106  	tmpDirF := requireOpenFile(t, tmpDir, experimentalsys.O_RDONLY, 0)
   107  	defer tmpDirF.Close()
   108  
   109  	t.Run("dir", func(t *testing.T) {
   110  		st, errno := tmpDirF.Stat()
   111  		require.EqualErrno(t, 0, errno)
   112  		requireDir(t, tmpDirF, st)
   113  		requireDevIno(t, tmpDirF, st)
   114  	})
   115  
   116  	// Windows allows you to stat a closed dir because it is accessed by path,
   117  	// not by file descriptor.
   118  	if runtime.GOOS != "windows" {
   119  		t.Run("closed dir", func(t *testing.T) {
   120  			require.EqualErrno(t, 0, tmpDirF.Close())
   121  			_, errno := tmpDirF.Stat()
   122  			require.EqualErrno(t, experimentalsys.EBADF, errno)
   123  		})
   124  	}
   125  
   126  	file := path.Join(tmpDir, "file")
   127  	require.NoError(t, os.WriteFile(file, nil, 0o400))
   128  	fileF := requireOpenFile(t, file, experimentalsys.O_RDONLY, 0)
   129  	defer fileF.Close()
   130  
   131  	t.Run("file", func(t *testing.T) {
   132  		st, errno := fileF.Stat()
   133  		require.EqualErrno(t, 0, errno)
   134  
   135  		require.False(t, st.Mode.IsDir())
   136  		require.NotEqual(t, uint64(0), st.Ino)
   137  	})
   138  
   139  	t.Run("closed fsFile", func(t *testing.T) {
   140  		require.EqualErrno(t, 0, fileF.Close())
   141  		_, errno := fileF.Stat()
   142  		require.EqualErrno(t, experimentalsys.EBADF, errno)
   143  	})
   144  
   145  	subdir := path.Join(tmpDir, "sub")
   146  	require.NoError(t, os.Mkdir(subdir, 0o500))
   147  	subdirF := requireOpenFile(t, subdir, experimentalsys.O_RDONLY, 0)
   148  	defer subdirF.Close()
   149  
   150  	t.Run("subdir", func(t *testing.T) {
   151  		st, errno := subdirF.Stat()
   152  		require.EqualErrno(t, 0, errno)
   153  		requireDir(t, subdirF, st)
   154  		requireDevIno(t, subdirF, st)
   155  	})
   156  
   157  	if runtime.GOOS != "windows" { // windows allows you to stat a closed dir
   158  		t.Run("closed subdir", func(t *testing.T) {
   159  			require.EqualErrno(t, 0, subdirF.Close())
   160  			_, errno := subdirF.Stat()
   161  			require.EqualErrno(t, experimentalsys.EBADF, errno)
   162  		})
   163  	}
   164  }
   165  
   166  func Test_StatFile_times(t *testing.T) {
   167  	tmpDir := t.TempDir()
   168  
   169  	file := path.Join(tmpDir, "file")
   170  	err := os.WriteFile(file, []byte{}, 0o700)
   171  	require.NoError(t, err)
   172  
   173  	type test struct {
   174  		name                 string
   175  		atimeNsec, mtimeNsec int64
   176  	}
   177  	// Note: This sets microsecond granularity because Windows doesn't support
   178  	// nanosecond.
   179  	tests := []test{
   180  		{
   181  			name:      "positive",
   182  			atimeNsec: time.Unix(123, 4*1e3).UnixNano(),
   183  			mtimeNsec: time.Unix(567, 8*1e3).UnixNano(),
   184  		},
   185  		{name: "zero"},
   186  	}
   187  
   188  	// linux and freebsd report inaccurate results when the input ts is negative.
   189  	if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
   190  		tests = append(tests,
   191  			test{
   192  				name:      "negative",
   193  				atimeNsec: time.Unix(-123, -4*1e3).UnixNano(),
   194  				mtimeNsec: time.Unix(-567, -8*1e3).UnixNano(),
   195  			},
   196  		)
   197  	}
   198  
   199  	for _, tt := range tests {
   200  		tc := tt
   201  		t.Run(tc.name, func(t *testing.T) {
   202  			err := os.Chtimes(file, time.UnixMicro(tc.atimeNsec/1e3), time.UnixMicro(tc.mtimeNsec/1e3))
   203  			require.NoError(t, err)
   204  
   205  			f := requireOpenFile(t, file, experimentalsys.O_RDONLY, 0)
   206  			defer f.Close()
   207  
   208  			st, errno := f.Stat()
   209  			require.EqualErrno(t, 0, errno)
   210  
   211  			require.Equal(t, st.Atim, tc.atimeNsec)
   212  			require.Equal(t, st.Mtim, tc.mtimeNsec)
   213  		})
   214  	}
   215  }
   216  
   217  func TestStatFile_dev_inode(t *testing.T) {
   218  	tmpDir := t.TempDir()
   219  	d := requireOpenFile(t, tmpDir, experimentalsys.O_RDONLY, 0)
   220  	defer d.Close()
   221  
   222  	path1 := path.Join(tmpDir, "1")
   223  	f1 := requireOpenFile(t, path1, experimentalsys.O_CREAT, 0o666)
   224  	defer f1.Close()
   225  
   226  	path2 := path.Join(tmpDir, "2")
   227  	f2 := requireOpenFile(t, path2, experimentalsys.O_CREAT, 0o666)
   228  	defer f2.Close()
   229  
   230  	pathLink2 := path.Join(tmpDir, "link2")
   231  	err := os.Symlink(path2, pathLink2)
   232  	require.NoError(t, err)
   233  	l2 := requireOpenFile(t, pathLink2, experimentalsys.O_RDONLY, 0)
   234  	defer l2.Close()
   235  
   236  	// First, stat the directory
   237  	st1, errno := d.Stat()
   238  	require.EqualErrno(t, 0, errno)
   239  	requireDir(t, d, st1)
   240  	requireDevIno(t, d, st1)
   241  
   242  	// Now, stat the files in it
   243  	st1, errno = f1.Stat()
   244  	require.EqualErrno(t, 0, errno)
   245  	requireNotDir(t, f1, st1)
   246  	requireDevIno(t, f1, st1)
   247  
   248  	st2, errno := f2.Stat()
   249  	require.EqualErrno(t, 0, errno)
   250  	requireNotDir(t, f2, st2)
   251  	requireDevIno(t, f2, st2)
   252  
   253  	st3, errno := l2.Stat()
   254  	require.EqualErrno(t, 0, errno)
   255  	requireNotDir(t, l2, st3)
   256  	requireDevIno(t, l2, st3)
   257  
   258  	// The files should be on the same device, but different inodes
   259  	require.Equal(t, st1.Dev, st2.Dev)
   260  	require.NotEqual(t, st1.Ino, st2.Ino)
   261  	require.Equal(t, st2, st3) // stat on a link is for its target
   262  
   263  	// Redoing stat should result in the same inodes
   264  	st1Again, errno := f1.Stat()
   265  	require.EqualErrno(t, 0, errno)
   266  	require.Equal(t, st1.Dev, st1Again.Dev)
   267  
   268  	// On Windows, we cannot rename while opening.
   269  	// So we manually close here before renaming.
   270  	require.EqualErrno(t, 0, f1.Close())
   271  	require.EqualErrno(t, 0, f2.Close())
   272  	require.EqualErrno(t, 0, l2.Close())
   273  
   274  	// Renaming a file shouldn't change its inodes.
   275  	require.EqualErrno(t, 0, rename(path1, path2))
   276  	f1 = requireOpenFile(t, path2, experimentalsys.O_RDONLY, 0)
   277  	defer f1.Close()
   278  
   279  	st1Again, errno = f1.Stat()
   280  	require.EqualErrno(t, 0, errno)
   281  	require.Equal(t, st1.Dev, st1Again.Dev)
   282  	require.Equal(t, st1.Ino, st1Again.Ino)
   283  }
   284  
   285  func requireNotDir(t *testing.T, d experimentalsys.File, st sys.Stat_t) {
   286  	// Verify cached state is correct
   287  	isDir, errno := d.IsDir()
   288  	require.EqualErrno(t, 0, errno)
   289  	require.False(t, isDir)
   290  	require.False(t, st.Mode.IsDir())
   291  }
   292  
   293  func requireDir(t *testing.T, d experimentalsys.File, st sys.Stat_t) {
   294  	// Verify cached state is correct
   295  	isDir, errno := d.IsDir()
   296  	require.EqualErrno(t, 0, errno)
   297  	require.True(t, isDir)
   298  	require.True(t, st.Mode.IsDir())
   299  }
   300  
   301  func requireDevIno(t *testing.T, f experimentalsys.File, st sys.Stat_t) {
   302  	// Results are inconsistent, so don't validate the opposite.
   303  	if statSetsIno() {
   304  		require.NotEqual(t, uint64(0), st.Dev)
   305  		require.NotEqual(t, uint64(0), st.Ino)
   306  	}
   307  
   308  	// Verify the special-cased properties supporting wasip2 "is_same_object"
   309  	// See https://github.com/WebAssembly/wasi-filesystem/pull/81
   310  	dev, errno := f.Dev()
   311  	require.EqualErrno(t, 0, errno)
   312  	require.Equal(t, st.Dev, dev)
   313  	ino, errno := f.Ino()
   314  	require.EqualErrno(t, 0, errno)
   315  	require.Equal(t, st.Ino, ino)
   316  }