github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/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/tetratelabs/wazero/experimental/sys" 11 "github.com/tetratelabs/wazero/internal/testing/require" 12 "github.com/tetratelabs/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 require.NotEqual(t, uint64(0), st.Dev) 304 require.NotEqual(t, uint64(0), st.Ino) 305 306 // Verify the special-cased properties supporting wasip2 "is_same_object" 307 // See https://github.com/WebAssembly/wasi-filesystem/pull/81 308 dev, errno := f.Dev() 309 require.EqualErrno(t, 0, errno) 310 require.Equal(t, st.Dev, dev) 311 ino, errno := f.Ino() 312 require.EqualErrno(t, 0, errno) 313 require.Equal(t, st.Ino, ino) 314 }