github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/internal/sys/fs_test.go (about) 1 package sys 2 3 import ( 4 "embed" 5 "errors" 6 "fmt" 7 "io/fs" 8 "os" 9 "path" 10 "testing" 11 gofstest "testing/fstest" 12 13 "github.com/tetratelabs/wazero/experimental/sys" 14 "github.com/tetratelabs/wazero/internal/sysfs" 15 testfs "github.com/tetratelabs/wazero/internal/testing/fs" 16 "github.com/tetratelabs/wazero/internal/testing/require" 17 ) 18 19 //go:embed testdata 20 var testdata embed.FS 21 22 func TestNewFSContext(t *testing.T) { 23 embedFS, err := fs.Sub(testdata, "testdata") 24 require.NoError(t, err) 25 26 dirfs := sysfs.DirFS(".") 27 28 // Test various usual configuration for the file system. 29 tests := []struct { 30 name string 31 fs sys.FS 32 }{ 33 { 34 name: "embed.FS", 35 fs: &sysfs.AdaptFS{FS: embedFS}, 36 }, 37 { 38 name: "DirFS", 39 // Don't use "testdata" because it may not be present in 40 // cross-architecture (a.k.a. scratch) build containers. 41 fs: dirfs, 42 }, 43 { 44 name: "ReadFS", 45 fs: &sysfs.ReadFS{FS: dirfs}, 46 }, 47 { 48 name: "fstest.MapFS", 49 fs: &sysfs.AdaptFS{FS: gofstest.MapFS{}}, 50 }, 51 } 52 53 for _, tt := range tests { 54 tc := tt 55 56 t.Run(tc.name, func(t *testing.T) { 57 for _, root := range []string{"/", ""} { 58 t.Run(fmt.Sprintf("root = '%s'", root), func(t *testing.T) { 59 c := Context{} 60 err := c.InitFSContext(nil, nil, nil, []sys.FS{tc.fs}, []string{root}, nil) 61 require.NoError(t, err) 62 fsc := c.fsc 63 defer fsc.Close() 64 65 rootFs := tc.fs 66 preopenedDir, _ := fsc.openedFiles.Lookup(FdPreopen) 67 require.Equal(t, tc.fs, rootFs) 68 require.NotNil(t, preopenedDir) 69 require.Equal(t, "/", preopenedDir.Name) 70 71 // Verify that each call to OpenFile returns a different file 72 // descriptor. 73 f1, errno := fsc.OpenFile(preopenedDir.FS, preopenedDir.Name, 0, 0) 74 require.EqualErrno(t, 0, errno) 75 require.NotEqual(t, FdPreopen, f1) 76 77 // Verify that file descriptors are reused. 78 // 79 // Note that this specific behavior is not required by WASI which 80 // only documents that file descriptor numbers will be selected 81 // randomly and applications should not rely on them. We added this 82 // test to ensure that our implementation properly reuses descriptor 83 // numbers but if we were to change the reuse strategy, this test 84 // would likely break and need to be updated. 85 require.EqualErrno(t, 0, fsc.CloseFile(f1)) 86 f2, errno := fsc.OpenFile(preopenedDir.FS, preopenedDir.Name, 0, 0) 87 require.EqualErrno(t, 0, errno) 88 require.Equal(t, f1, f2) 89 }) 90 } 91 }) 92 93 } 94 } 95 96 func TestFSContext_CloseFile(t *testing.T) { 97 embedFS, err := fs.Sub(testdata, "testdata") 98 require.NoError(t, err) 99 testFS := &sysfs.AdaptFS{FS: embedFS} 100 101 c := Context{} 102 err = c.InitFSContext(nil, nil, nil, []sys.FS{testFS}, []string{"/"}, nil) 103 require.NoError(t, err) 104 fsc := c.fsc 105 defer fsc.Close() 106 107 fdToClose, errno := fsc.OpenFile(testFS, "empty.txt", sys.O_RDONLY, 0) 108 require.EqualErrno(t, 0, errno) 109 110 fdToKeep, errno := fsc.OpenFile(testFS, "test.txt", sys.O_RDONLY, 0) 111 require.EqualErrno(t, 0, errno) 112 113 // Close 114 require.EqualErrno(t, 0, fsc.CloseFile(fdToClose)) 115 116 // Verify fdToClose is closed and removed from the opened FDs. 117 _, ok := fsc.LookupFile(fdToClose) 118 require.False(t, ok) 119 120 // Verify fdToKeep is not closed 121 _, ok = fsc.LookupFile(fdToKeep) 122 require.True(t, ok) 123 124 t.Run("EBADF for an invalid FD", func(t *testing.T) { 125 require.EqualErrno(t, sys.EBADF, fsc.CloseFile(42)) // 42 is an arbitrary invalid FD 126 }) 127 t.Run("Can close a pre-open", func(t *testing.T) { 128 require.EqualErrno(t, 0, fsc.CloseFile(FdPreopen)) 129 }) 130 } 131 132 func TestFSContext_noPreopens(t *testing.T) { 133 c := Context{} 134 err := c.InitFSContext(nil, nil, nil, nil, nil, nil) 135 require.NoError(t, err) 136 testFS := &c.fsc 137 require.NoError(t, err) 138 139 expected := &FSContext{} 140 noopStdin, _ := stdinFileEntry(nil) 141 expected.openedFiles.Insert(noopStdin) 142 noopStdout, _ := stdioWriterFileEntry("stdout", nil) 143 expected.openedFiles.Insert(noopStdout) 144 noopStderr, _ := stdioWriterFileEntry("stderr", nil) 145 expected.openedFiles.Insert(noopStderr) 146 147 t.Run("Close closes", func(t *testing.T) { 148 err := testFS.Close() 149 require.NoError(t, err) 150 151 // Closes opened files 152 require.Equal(t, &FSContext{}, testFS) 153 }) 154 } 155 156 func TestContext_Close(t *testing.T) { 157 testFS := &sysfs.AdaptFS{FS: testfs.FS{"foo": &testfs.File{}}} 158 159 c := Context{} 160 err := c.InitFSContext(nil, nil, nil, []sys.FS{testFS}, []string{"/"}, nil) 161 require.NoError(t, err) 162 fsc := c.fsc 163 164 // Verify base case 165 require.Equal(t, 1+FdPreopen, int32(fsc.openedFiles.Len())) 166 167 _, errno := fsc.OpenFile(testFS, "foo", sys.O_RDONLY, 0) 168 require.EqualErrno(t, 0, errno) 169 require.Equal(t, 2+FdPreopen, int32(fsc.openedFiles.Len())) 170 171 // Closing should not err. 172 require.NoError(t, fsc.Close()) 173 174 // Verify our intended side-effect 175 require.Zero(t, fsc.openedFiles.Len()) 176 177 // Verify no error closing again. 178 require.NoError(t, fsc.Close()) 179 } 180 181 func TestContext_Close_Error(t *testing.T) { 182 file := &testfs.File{CloseErr: errors.New("error closing")} 183 184 testFS := &sysfs.AdaptFS{FS: testfs.FS{"foo": file}} 185 186 c := Context{} 187 err := c.InitFSContext(nil, nil, nil, []sys.FS{testFS}, []string{"/"}, nil) 188 require.NoError(t, err) 189 fsc := c.fsc 190 191 // open another file 192 _, errno := fsc.OpenFile(testFS, "foo", sys.O_RDONLY, 0) 193 require.EqualErrno(t, 0, errno) 194 195 // arbitrary errors coerce to EIO 196 require.EqualErrno(t, sys.EIO, fsc.Close()) 197 198 // Paths should clear even under error 199 require.Zero(t, fsc.openedFiles.Len(), "expected no opened files") 200 } 201 202 func TestFSContext_Renumber(t *testing.T) { 203 tmpDir := t.TempDir() 204 dirFS := sysfs.DirFS(tmpDir) 205 206 const dirName = "dir" 207 errno := dirFS.Mkdir(dirName, 0o700) 208 require.EqualErrno(t, 0, errno) 209 210 c := Context{} 211 err := c.InitFSContext(nil, nil, nil, []sys.FS{dirFS}, []string{"/"}, nil) 212 require.NoError(t, err) 213 fsc := c.fsc 214 215 defer fsc.Close() 216 217 for _, toFd := range []int32{10, 100, 100} { 218 fromFd, errno := fsc.OpenFile(dirFS, dirName, sys.O_RDONLY, 0) 219 require.EqualErrno(t, 0, errno) 220 221 prevDirFile, ok := fsc.LookupFile(fromFd) 222 require.True(t, ok) 223 224 require.EqualErrno(t, 0, fsc.Renumber(fromFd, toFd)) 225 226 renumberedDirFile, ok := fsc.LookupFile(toFd) 227 require.True(t, ok) 228 229 require.Equal(t, prevDirFile, renumberedDirFile) 230 231 // Previous file descriptor shouldn't be used. 232 _, ok = fsc.LookupFile(fromFd) 233 require.False(t, ok) 234 } 235 236 t.Run("errors", func(t *testing.T) { 237 // Sanity check for 3 being preopen. 238 preopen, ok := fsc.LookupFile(3) 239 require.True(t, ok) 240 require.True(t, preopen.IsPreopen) 241 242 // From is preopen. 243 require.Equal(t, sys.ENOTSUP, fsc.Renumber(3, 100)) 244 245 // From does not exist. 246 require.Equal(t, sys.EBADF, fsc.Renumber(12345, 3)) 247 248 // Both are preopen. 249 require.Equal(t, sys.ENOTSUP, fsc.Renumber(3, 3)) 250 }) 251 } 252 253 // This is similar to https://github.com/WebAssembly/wasi-testsuite/blob/ac32f57400cdcdd0425d3085c24fc7fc40011d1c/tests/rust/src/bin/fd_readdir.rs#L120 254 func TestDirentCache_ReadNewFile(t *testing.T) { 255 tmpDir := t.TempDir() 256 257 c := Context{} 258 root := sysfs.DirFS(tmpDir) 259 err := c.InitFSContext(nil, nil, nil, []sys.FS{root}, []string{"/"}, nil) 260 require.NoError(t, err) 261 fsc := c.fsc 262 defer fsc.Close() 263 264 fd, errno := fsc.OpenFile(root, ".", sys.O_RDONLY, 0) 265 require.EqualErrno(t, 0, errno) 266 defer fsc.CloseFile(fd) // nolint 267 f, _ := fsc.LookupFile(fd) 268 269 dir, errno := f.DirentCache() 270 require.EqualErrno(t, 0, errno) 271 272 // Read the empty directory, which should only have the dot entries. 273 dirents, errno := dir.Read(0, 5) 274 require.EqualErrno(t, 0, errno) 275 require.Equal(t, 2, len(dirents)) 276 require.Equal(t, ".", dirents[0].Name) 277 require.Equal(t, "..", dirents[1].Name) 278 279 // Write a new file to the directory 280 require.NoError(t, os.WriteFile(path.Join(tmpDir, "file"), nil, 0o0666)) 281 282 // Read it again, which should see the new file. 283 dirents, errno = dir.Read(0, 5) 284 require.EqualErrno(t, 0, errno) 285 require.Equal(t, 3, len(dirents)) 286 require.Equal(t, ".", dirents[0].Name) 287 require.Equal(t, "..", dirents[1].Name) 288 require.Equal(t, "file", dirents[2].Name) 289 290 // Read it again, using the file position. 291 filePos := uint64(2) 292 dirents, errno = dir.Read(filePos, 3) 293 require.EqualErrno(t, 0, errno) 294 require.Equal(t, 1, len(dirents)) 295 require.Equal(t, "file", dirents[0].Name) 296 } 297 298 func TestStripPrefixesAndTrailingSlash(t *testing.T) { 299 tests := []struct { 300 path, expected string 301 }{ 302 { 303 path: ".", 304 expected: "", 305 }, 306 { 307 path: "/", 308 expected: "", 309 }, 310 { 311 path: "./", 312 expected: "", 313 }, 314 { 315 path: "./foo", 316 expected: "foo", 317 }, 318 { 319 path: ".foo", 320 expected: ".foo", 321 }, 322 { 323 path: "././foo", 324 expected: "foo", 325 }, 326 { 327 path: "/foo", 328 expected: "foo", 329 }, 330 { 331 path: "foo/", 332 expected: "foo", 333 }, 334 { 335 path: "//", 336 expected: "", 337 }, 338 { 339 path: "../../", 340 expected: "../..", 341 }, 342 { 343 path: "./../../", 344 expected: "../..", 345 }, 346 } 347 348 for _, tt := range tests { 349 tc := tt 350 351 t.Run(tc.path, func(t *testing.T) { 352 path := StripPrefixesAndTrailingSlash(tc.path) 353 require.Equal(t, tc.expected, path) 354 }) 355 } 356 }