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