github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/sysfs/file_test.go (about) 1 package sysfs 2 3 import ( 4 "embed" 5 "io" 6 "io/fs" 7 "os" 8 "path" 9 "runtime" 10 "testing" 11 gofstest "testing/fstest" 12 13 experimentalsys "github.com/wasilibs/wazerox/experimental/sys" 14 "github.com/wasilibs/wazerox/internal/fsapi" 15 "github.com/wasilibs/wazerox/internal/platform" 16 "github.com/wasilibs/wazerox/internal/testing/require" 17 "github.com/wasilibs/wazerox/sys" 18 ) 19 20 //go:embed file_test.go 21 var embedFS embed.FS 22 23 var ( 24 //go:embed testdata 25 testdata embed.FS 26 wazeroFile = "wazero.txt" 27 emptyFile = "empty.txt" 28 ) 29 30 func TestStdioFileSetNonblock(t *testing.T) { 31 // Test using os.Pipe as it is known to support non-blocking reads. 32 r, w, err := os.Pipe() 33 require.NoError(t, err) 34 defer r.Close() 35 defer w.Close() 36 37 rF, err := NewStdioFile(true, r) 38 require.NoError(t, err) 39 40 errno := rF.SetNonblock(true) 41 require.EqualErrno(t, 0, errno) 42 require.True(t, rF.IsNonblock()) 43 44 errno = rF.SetNonblock(false) 45 require.EqualErrno(t, 0, errno) 46 require.False(t, rF.IsNonblock()) 47 } 48 49 func TestRegularFileSetNonblock(t *testing.T) { 50 // Test using os.Pipe as it is known to support non-blocking reads. 51 r, w, err := os.Pipe() 52 require.NoError(t, err) 53 defer r.Close() 54 defer w.Close() 55 56 rF := newOsFile("", experimentalsys.O_RDONLY, 0, r) 57 58 errno := rF.SetNonblock(true) 59 require.EqualErrno(t, 0, errno) 60 require.True(t, rF.IsNonblock()) 61 62 // Read from the file without ever writing to it should not block. 63 buf := make([]byte, 8) 64 _, e := rF.Read(buf) 65 require.EqualErrno(t, experimentalsys.EAGAIN, e) 66 67 errno = rF.SetNonblock(false) 68 require.EqualErrno(t, 0, errno) 69 require.False(t, rF.IsNonblock()) 70 } 71 72 func TestReadFdNonblock(t *testing.T) { 73 // Test using os.Pipe as it is known to support non-blocking reads. 74 r, w, err := os.Pipe() 75 require.NoError(t, err) 76 defer r.Close() 77 defer w.Close() 78 79 fd := r.Fd() 80 errno := setNonblock(fd, true) 81 require.EqualErrno(t, 0, errno) 82 83 // Read from the file without ever writing to it should not block. 84 buf := make([]byte, 8) 85 _, errno = readFd(fd, buf) 86 require.EqualErrno(t, experimentalsys.EAGAIN, errno) 87 } 88 89 func TestWriteFdNonblock(t *testing.T) { 90 // Test using os.Pipe as it is known to support non-blocking reads. 91 r, w, err := os.Pipe() 92 require.NoError(t, err) 93 defer r.Close() 94 defer w.Close() 95 96 fd := w.Fd() 97 errno := setNonblock(fd, true) 98 99 require.EqualErrno(t, 0, errno) 100 101 // Create a buffer (the content is not relevant) 102 buf := make([]byte, 1024) 103 // Write to the file until the pipe buffer gets filled up. 104 numWrites := 100 105 for i := 0; i < numWrites; i++ { 106 _, e := writeFd(fd, buf) 107 if e != 0 { 108 if runtime.GOOS == "windows" { 109 // This is currently not supported on Windows. 110 require.EqualErrno(t, experimentalsys.ENOSYS, e) 111 } else { 112 require.EqualErrno(t, experimentalsys.EAGAIN, e) 113 } 114 return 115 } 116 } 117 t.Fatal("writeFd should return EAGAIN at some point") 118 } 119 120 func TestFileSetAppend(t *testing.T) { 121 tmpDir := t.TempDir() 122 123 fPath := path.Join(tmpDir, "file") 124 require.NoError(t, os.WriteFile(fPath, []byte("0123456789"), 0o600)) 125 126 // Open without APPEND. 127 f, errno := OpenOSFile(fPath, experimentalsys.O_RDWR, 0o600) 128 require.EqualErrno(t, 0, errno) 129 require.False(t, f.IsAppend()) 130 131 // Set the APPEND flag. 132 require.EqualErrno(t, 0, f.SetAppend(true)) 133 require.True(t, f.IsAppend()) 134 135 requireFileContent := func(exp string) { 136 buf, err := os.ReadFile(fPath) 137 require.NoError(t, err) 138 require.Equal(t, exp, string(buf)) 139 } 140 141 // with O_APPEND flag, the data is appended to buffer. 142 _, errno = f.Write([]byte("wazero")) 143 require.EqualErrno(t, 0, errno) 144 requireFileContent("0123456789wazero") 145 146 // Remove the APPEND flag. 147 require.EqualErrno(t, 0, f.SetAppend(false)) 148 require.False(t, f.IsAppend()) 149 150 _, errno = f.Seek(0, 0) 151 require.EqualErrno(t, 0, errno) 152 153 // without O_APPEND flag, the data writes at offset zero 154 _, errno = f.Write([]byte("wazero")) 155 require.EqualErrno(t, 0, errno) 156 requireFileContent("wazero6789wazero") 157 } 158 159 func TestStdioFile_SetAppend(t *testing.T) { 160 // SetAppend should not affect Stdio. 161 file, err := NewStdioFile(false, os.Stdout) 162 require.NoError(t, err) 163 errno := file.SetAppend(true) 164 require.EqualErrno(t, 0, errno) 165 _, errno = file.Write([]byte{}) 166 require.EqualErrno(t, 0, errno) 167 } 168 169 func TestFileIno(t *testing.T) { 170 tmpDir := t.TempDir() 171 dirFS, embedFS, mapFS := dirEmbedMapFS(t, tmpDir) 172 173 // get the expected inode 174 st, errno := stat(tmpDir) 175 require.EqualErrno(t, 0, errno) 176 177 tests := []struct { 178 name string 179 fs fs.FS 180 expectedIno sys.Inode 181 }{ 182 {name: "os.DirFS", fs: dirFS, expectedIno: st.Ino}, 183 {name: "embed.api.FS", fs: embedFS}, 184 {name: "fstest.MapFS", fs: mapFS}, 185 } 186 187 for _, tc := range tests { 188 tc := tc 189 190 t.Run(tc.name, func(t *testing.T) { 191 d, errno := OpenFSFile(tc.fs, ".", experimentalsys.O_RDONLY, 0) 192 require.EqualErrno(t, 0, errno) 193 defer d.Close() 194 195 ino, errno := d.Ino() 196 require.EqualErrno(t, 0, errno) 197 // Results are inconsistent, so don't validate the opposite. 198 if statSetsIno() { 199 require.Equal(t, tc.expectedIno, ino) 200 } 201 }) 202 } 203 204 t.Run("OS", func(t *testing.T) { 205 d, errno := OpenOSFile(tmpDir, experimentalsys.O_RDONLY, 0) 206 require.EqualErrno(t, 0, errno) 207 defer d.Close() 208 209 ino, errno := d.Ino() 210 require.EqualErrno(t, 0, errno) 211 // Results are inconsistent, so don't validate the opposite. 212 if statSetsIno() { 213 require.Equal(t, st.Ino, ino) 214 } 215 }) 216 } 217 218 // statSetsIno returns true if this will set sys.Stat_t Ino on stat. The 219 // reverse doesn't mean it won't. Rather it is inconsistent. This is needed 220 // because Windows on Go 1.19 sometimes, but not always returns non-zero inode. 221 func statSetsIno() bool { 222 if runtime.GOOS != "windows" { 223 return true 224 } else { 225 // Go can read the inode via a Windows file handle, but it is 226 // inconsistent on Go 1.19. 227 return platform.IsAtLeastGo120 228 } 229 } 230 231 func TestFileIsDir(t *testing.T) { 232 dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir()) 233 234 tests := []struct { 235 name string 236 fs fs.FS 237 }{ 238 {name: "os.DirFS", fs: dirFS}, 239 {name: "embed.api.FS", fs: embedFS}, 240 {name: "fstest.MapFS", fs: mapFS}, 241 } 242 243 for _, tc := range tests { 244 tc := tc 245 246 t.Run(tc.name, func(t *testing.T) { 247 t.Run("file", func(t *testing.T) { 248 f, errno := OpenFSFile(tc.fs, wazeroFile, experimentalsys.O_RDONLY, 0) 249 require.EqualErrno(t, 0, errno) 250 defer f.Close() 251 252 isDir, errno := f.IsDir() 253 require.EqualErrno(t, 0, errno) 254 require.False(t, isDir) 255 }) 256 257 t.Run("dir", func(t *testing.T) { 258 d, errno := OpenFSFile(tc.fs, ".", experimentalsys.O_RDONLY, 0) 259 require.EqualErrno(t, 0, errno) 260 defer d.Close() 261 262 isDir, errno := d.IsDir() 263 require.EqualErrno(t, 0, errno) 264 require.True(t, isDir) 265 }) 266 }) 267 } 268 269 t.Run("OS dir", func(t *testing.T) { 270 d, errno := OpenOSFile(t.TempDir(), experimentalsys.O_RDONLY, 0) 271 require.EqualErrno(t, 0, errno) 272 defer d.Close() 273 274 isDir, errno := d.IsDir() 275 require.EqualErrno(t, 0, errno) 276 require.True(t, isDir) 277 }) 278 } 279 280 func TestFileReadAndPread(t *testing.T) { 281 dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir()) 282 283 tests := []struct { 284 name string 285 fs fs.FS 286 }{ 287 {name: "os.DirFS", fs: dirFS}, 288 {name: "embed.api.FS", fs: embedFS}, 289 {name: "fstest.MapFS", fs: mapFS}, 290 } 291 292 buf := make([]byte, 3) 293 294 for _, tc := range tests { 295 tc := tc 296 297 t.Run(tc.name, func(t *testing.T) { 298 f, errno := OpenFSFile(tc.fs, wazeroFile, experimentalsys.O_RDONLY, 0) 299 require.EqualErrno(t, 0, errno) 300 defer f.Close() 301 302 // The file should be readable (base case) 303 requireRead(t, f, buf) 304 require.Equal(t, "waz", string(buf)) 305 buf = buf[:] 306 307 // We should be able to pread from zero also 308 requirePread(t, f, buf, 0) 309 require.Equal(t, "waz", string(buf)) 310 buf = buf[:] 311 312 // If the offset didn't change, read should expect the next three chars. 313 requireRead(t, f, buf) 314 require.Equal(t, "ero", string(buf)) 315 buf = buf[:] 316 317 // We should also be able pread from any offset 318 requirePread(t, f, buf, 2) 319 require.Equal(t, "zer", string(buf)) 320 }) 321 } 322 } 323 324 func TestFilePoll_POLLIN(t *testing.T) { 325 pflag := fsapi.POLLIN 326 327 // Test using os.Pipe as it is known to support poll. 328 r, w, err := os.Pipe() 329 require.NoError(t, err) 330 defer r.Close() 331 defer w.Close() 332 333 rF, err := NewStdioFile(true, r) 334 require.NoError(t, err) 335 buf := make([]byte, 10) 336 timeout := int32(0) // return immediately 337 338 // When there's nothing in the pipe, it isn't ready. 339 ready, errno := rF.Poll(pflag, timeout) 340 require.EqualErrno(t, 0, errno) 341 require.False(t, ready) 342 343 // Write to the pipe to make the data available 344 expected := []byte("wazero") 345 _, err = w.Write([]byte("wazero")) 346 require.NoError(t, err) 347 348 // We should now be able to poll ready 349 ready, errno = rF.Poll(pflag, timeout) 350 require.EqualErrno(t, 0, errno) 351 require.True(t, ready) 352 353 // We should now be able to read from the pipe 354 n, errno := rF.Read(buf) 355 require.EqualErrno(t, 0, errno) 356 require.Equal(t, len(expected), n) 357 require.Equal(t, expected, buf[:len(expected)]) 358 } 359 360 func TestFilePoll_POLLOUT(t *testing.T) { 361 pflag := fsapi.POLLOUT 362 363 // Test using os.Pipe as it is known to support poll. 364 r, w, err := os.Pipe() 365 require.NoError(t, err) 366 defer r.Close() 367 defer w.Close() 368 369 wF, err := NewStdioFile(false, w) 370 require.NoError(t, err) 371 timeout := int32(0) // return immediately 372 373 // We don't yet implement write blocking. 374 ready, errno := wF.Poll(pflag, timeout) 375 require.EqualErrno(t, experimentalsys.ENOTSUP, errno) 376 require.False(t, ready) 377 } 378 379 func requireRead(t *testing.T, f experimentalsys.File, buf []byte) { 380 n, errno := f.Read(buf) 381 require.EqualErrno(t, 0, errno) 382 require.Equal(t, len(buf), n) 383 } 384 385 func requirePread(t *testing.T, f experimentalsys.File, buf []byte, off int64) { 386 n, errno := f.Pread(buf, off) 387 require.EqualErrno(t, 0, errno) 388 require.Equal(t, len(buf), n) 389 } 390 391 func TestFileRead_empty(t *testing.T) { 392 dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir()) 393 394 tests := []struct { 395 name string 396 fs fs.FS 397 }{ 398 {name: "os.DirFS", fs: dirFS}, 399 {name: "embed.api.FS", fs: embedFS}, 400 {name: "fstest.MapFS", fs: mapFS}, 401 } 402 403 buf := make([]byte, 3) 404 405 for _, tc := range tests { 406 tc := tc 407 408 t.Run(tc.name, func(t *testing.T) { 409 f, errno := OpenFSFile(tc.fs, emptyFile, experimentalsys.O_RDONLY, 0) 410 require.EqualErrno(t, 0, errno) 411 defer f.Close() 412 413 t.Run("Read", func(t *testing.T) { 414 // We should be able to read an empty file 415 n, errno := f.Read(buf) 416 require.EqualErrno(t, 0, errno) 417 require.Zero(t, n) 418 }) 419 420 t.Run("Pread", func(t *testing.T) { 421 n, errno := f.Pread(buf, 0) 422 require.EqualErrno(t, 0, errno) 423 require.Zero(t, n) 424 }) 425 }) 426 } 427 } 428 429 type maskFS struct { 430 fs.FS 431 } 432 433 func (m *maskFS) Open(name string) (fs.File, error) { 434 f, err := m.FS.Open(name) 435 return struct{ fs.File }{f}, err 436 } 437 438 func TestFilePread_Unsupported(t *testing.T) { 439 embedFS, err := fs.Sub(testdata, "testdata") 440 require.NoError(t, err) 441 442 f, errno := OpenFSFile(&maskFS{embedFS}, emptyFile, experimentalsys.O_RDONLY, 0) 443 require.EqualErrno(t, 0, errno) 444 defer f.Close() 445 446 buf := make([]byte, 3) 447 _, errno = f.Pread(buf, 0) 448 require.EqualErrno(t, experimentalsys.ENOSYS, errno) 449 } 450 451 func TestFileRead_Errors(t *testing.T) { 452 // Create the file 453 path := path.Join(t.TempDir(), emptyFile) 454 455 // Open the file write-only 456 flag := experimentalsys.O_WRONLY | experimentalsys.O_CREAT 457 f := requireOpenFile(t, path, flag, 0o600) 458 defer f.Close() 459 buf := make([]byte, 5) 460 461 tests := []struct { 462 name string 463 fn func(experimentalsys.File) experimentalsys.Errno 464 }{ 465 {name: "Read", fn: func(f experimentalsys.File) experimentalsys.Errno { 466 _, errno := f.Read(buf) 467 return errno 468 }}, 469 {name: "Pread", fn: func(f experimentalsys.File) experimentalsys.Errno { 470 _, errno := f.Pread(buf, 0) 471 return errno 472 }}, 473 } 474 475 for _, tc := range tests { 476 tc := tc 477 478 t.Run(tc.name, func(t *testing.T) { 479 t.Run("EBADF when not open for reading", func(t *testing.T) { 480 // The descriptor exists, but not open for reading 481 errno := tc.fn(f) 482 require.EqualErrno(t, experimentalsys.EBADF, errno) 483 }) 484 testEISDIR(t, tc.fn) 485 }) 486 } 487 } 488 489 func TestFileSeek(t *testing.T) { 490 tmpDir := t.TempDir() 491 dirFS, embedFS, mapFS := dirEmbedMapFS(t, tmpDir) 492 493 tests := []struct { 494 name string 495 openFile func(string) (experimentalsys.File, experimentalsys.Errno) 496 }{ 497 {name: "fsFile os.DirFS", openFile: func(name string) (experimentalsys.File, experimentalsys.Errno) { 498 return OpenFSFile(dirFS, name, experimentalsys.O_RDONLY, 0) 499 }}, 500 {name: "fsFile embed.api.FS", openFile: func(name string) (experimentalsys.File, experimentalsys.Errno) { 501 return OpenFSFile(embedFS, name, experimentalsys.O_RDONLY, 0) 502 }}, 503 {name: "fsFile fstest.MapFS", openFile: func(name string) (experimentalsys.File, experimentalsys.Errno) { 504 return OpenFSFile(mapFS, name, experimentalsys.O_RDONLY, 0) 505 }}, 506 {name: "osFile", openFile: func(name string) (experimentalsys.File, experimentalsys.Errno) { 507 return OpenOSFile(path.Join(tmpDir, name), experimentalsys.O_RDONLY, 0o666) 508 }}, 509 } 510 511 buf := make([]byte, 3) 512 513 for _, tc := range tests { 514 tc := tc 515 516 t.Run(tc.name, func(t *testing.T) { 517 f, errno := tc.openFile(wazeroFile) 518 require.EqualErrno(t, 0, errno) 519 defer f.Close() 520 521 // Shouldn't be able to use an invalid whence 522 _, errno = f.Seek(0, io.SeekEnd+1) 523 require.EqualErrno(t, experimentalsys.EINVAL, errno) 524 _, errno = f.Seek(0, -1) 525 require.EqualErrno(t, experimentalsys.EINVAL, errno) 526 527 // Shouldn't be able to seek before the file starts. 528 _, errno = f.Seek(-1, io.SeekStart) 529 require.EqualErrno(t, experimentalsys.EINVAL, errno) 530 531 requireRead(t, f, buf) // read 3 bytes 532 533 // Seek to the start 534 newOffset, errno := f.Seek(0, io.SeekStart) 535 require.EqualErrno(t, 0, errno) 536 537 // verify we can re-read from the beginning now. 538 require.Zero(t, newOffset) 539 requireRead(t, f, buf) // read 3 bytes again 540 require.Equal(t, "waz", string(buf)) 541 buf = buf[:] 542 543 // Seek to the start with zero allows you to read it back. 544 newOffset, errno = f.Seek(0, io.SeekCurrent) 545 require.EqualErrno(t, 0, errno) 546 require.Equal(t, int64(3), newOffset) 547 548 // Seek to the last two bytes 549 newOffset, errno = f.Seek(-2, io.SeekEnd) 550 require.EqualErrno(t, 0, errno) 551 552 // verify we can read the last two bytes 553 require.Equal(t, int64(5), newOffset) 554 n, errno := f.Read(buf) 555 require.EqualErrno(t, 0, errno) 556 require.Equal(t, 2, n) 557 require.Equal(t, "o\n", string(buf[:2])) 558 559 t.Run("directory seek to zero", func(t *testing.T) { 560 dotF, errno := tc.openFile(".") 561 require.EqualErrno(t, 0, errno) 562 defer dotF.Close() 563 564 dirents, errno := dotF.Readdir(-1) 565 require.EqualErrno(t, 0, errno) 566 direntCount := len(dirents) 567 require.False(t, direntCount == 0) 568 569 // rewind via seek to zero 570 newOffset, errno := dotF.Seek(0, io.SeekStart) 571 require.EqualErrno(t, 0, errno) 572 require.Zero(t, newOffset) 573 574 // redundantly seek to zero again 575 newOffset, errno = dotF.Seek(0, io.SeekStart) 576 require.EqualErrno(t, 0, errno) 577 require.Zero(t, newOffset) 578 579 // We should be able to read again 580 dirents, errno = dotF.Readdir(-1) 581 require.EqualErrno(t, 0, errno) 582 require.Equal(t, direntCount, len(dirents)) 583 }) 584 585 seekToZero := func(f experimentalsys.File) experimentalsys.Errno { 586 _, errno := f.Seek(0, io.SeekStart) 587 return errno 588 } 589 testEBADFIfFileClosed(t, seekToZero) 590 }) 591 } 592 } 593 594 func requireSeek(t *testing.T, f experimentalsys.File, off int64, whence int) int64 { 595 n, errno := f.Seek(off, whence) 596 require.EqualErrno(t, 0, errno) 597 return n 598 } 599 600 func TestFileSeek_empty(t *testing.T) { 601 dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir()) 602 603 tests := []struct { 604 name string 605 fs fs.FS 606 }{ 607 {name: "os.DirFS", fs: dirFS}, 608 {name: "embed.api.FS", fs: embedFS}, 609 {name: "fstest.MapFS", fs: mapFS}, 610 } 611 612 for _, tc := range tests { 613 tc := tc 614 615 t.Run(tc.name, func(t *testing.T) { 616 f, errno := OpenFSFile(tc.fs, emptyFile, experimentalsys.O_RDONLY, 0) 617 require.EqualErrno(t, 0, errno) 618 defer f.Close() 619 620 t.Run("Start", func(t *testing.T) { 621 require.Zero(t, requireSeek(t, f, 0, io.SeekStart)) 622 }) 623 624 t.Run("Current", func(t *testing.T) { 625 require.Zero(t, requireSeek(t, f, 0, io.SeekCurrent)) 626 }) 627 628 t.Run("End", func(t *testing.T) { 629 require.Zero(t, requireSeek(t, f, 0, io.SeekEnd)) 630 }) 631 }) 632 } 633 } 634 635 func TestFileSeek_Unsupported(t *testing.T) { 636 embedFS, err := fs.Sub(testdata, "testdata") 637 require.NoError(t, err) 638 639 f, errno := OpenFSFile(&maskFS{embedFS}, emptyFile, experimentalsys.O_RDONLY, 0) 640 require.EqualErrno(t, 0, errno) 641 defer f.Close() 642 643 _, errno = f.Seek(0, io.SeekCurrent) 644 require.EqualErrno(t, experimentalsys.ENOSYS, errno) 645 } 646 647 func TestFileWriteAndPwrite(t *testing.T) { 648 // sys.FS doesn't support writes, and there is no other built-in 649 // implementation except os.File. 650 path := path.Join(t.TempDir(), wazeroFile) 651 f := requireOpenFile(t, path, experimentalsys.O_RDWR|experimentalsys.O_CREAT, 0o600) 652 defer f.Close() 653 654 text := "wazero" 655 buf := make([]byte, 3) 656 copy(buf, text[:3]) 657 658 // The file should be writeable 659 requireWrite(t, f, buf) 660 661 // We should be able to pwrite at gap 662 requirePwrite(t, f, buf, 6) 663 664 copy(buf, text[3:]) 665 666 // If the offset didn't change, the next chars will write after the 667 // first 668 requireWrite(t, f, buf) 669 670 // We should be able to pwrite the same bytes as above 671 requirePwrite(t, f, buf, 9) 672 673 // We should also be able to pwrite past the above. 674 requirePwrite(t, f, buf, 12) 675 676 b, err := os.ReadFile(path) 677 require.NoError(t, err) 678 679 // We expect to have written the text two and a half times: 680 // 1. Write: (file offset 0) "waz" 681 // 2. Pwrite: offset 6 "waz" 682 // 3. Write: (file offset 3) "ero" 683 // 4. Pwrite: offset 9 "ero" 684 // 4. Pwrite: offset 12 "ero" 685 require.Equal(t, "wazerowazeroero", string(b)) 686 } 687 688 func requireWrite(t *testing.T, f experimentalsys.File, buf []byte) { 689 n, errno := f.Write(buf) 690 require.EqualErrno(t, 0, errno) 691 require.Equal(t, len(buf), n) 692 } 693 694 func requirePwrite(t *testing.T, f experimentalsys.File, buf []byte, off int64) { 695 n, errno := f.Pwrite(buf, off) 696 require.EqualErrno(t, 0, errno) 697 require.Equal(t, len(buf), n) 698 } 699 700 func TestFileWrite_empty(t *testing.T) { 701 // sys.FS doesn't support writes, and there is no other built-in 702 // implementation except os.File. 703 path := path.Join(t.TempDir(), emptyFile) 704 f := requireOpenFile(t, path, experimentalsys.O_RDWR|experimentalsys.O_CREAT, 0o600) 705 defer f.Close() 706 707 tests := []struct { 708 name string 709 fn func(experimentalsys.File, []byte) (int, experimentalsys.Errno) 710 }{ 711 {name: "Write", fn: func(f experimentalsys.File, buf []byte) (int, experimentalsys.Errno) { 712 return f.Write(buf) 713 }}, 714 {name: "Pwrite from zero", fn: func(f experimentalsys.File, buf []byte) (int, experimentalsys.Errno) { 715 return f.Pwrite(buf, 0) 716 }}, 717 {name: "Pwrite from 3", fn: func(f experimentalsys.File, buf []byte) (int, experimentalsys.Errno) { 718 return f.Pwrite(buf, 3) 719 }}, 720 } 721 722 var emptyBuf []byte 723 724 for _, tc := range tests { 725 tc := tc 726 727 t.Run(tc.name, func(t *testing.T) { 728 n, errno := tc.fn(f, emptyBuf) 729 require.EqualErrno(t, 0, errno) 730 require.Zero(t, n) 731 732 // The file should be empty 733 b, err := os.ReadFile(path) 734 require.NoError(t, err) 735 require.Zero(t, len(b)) 736 }) 737 } 738 } 739 740 func TestFileWrite_Unsupported(t *testing.T) { 741 embedFS, err := fs.Sub(testdata, "testdata") 742 require.NoError(t, err) 743 744 // Use sys.O_RDWR so that it fails due to type not flags 745 f, errno := OpenFSFile(&maskFS{embedFS}, wazeroFile, experimentalsys.O_RDWR, 0) 746 require.EqualErrno(t, 0, errno) 747 defer f.Close() 748 749 tests := []struct { 750 name string 751 fn func(experimentalsys.File, []byte) (int, experimentalsys.Errno) 752 }{ 753 {name: "Write", fn: func(f experimentalsys.File, buf []byte) (int, experimentalsys.Errno) { 754 return f.Write(buf) 755 }}, 756 {name: "Pwrite", fn: func(f experimentalsys.File, buf []byte) (int, experimentalsys.Errno) { 757 return f.Pwrite(buf, 0) 758 }}, 759 } 760 761 buf := []byte("wazero") 762 763 for _, tc := range tests { 764 tc := tc 765 766 t.Run(tc.name, func(t *testing.T) { 767 _, errno := tc.fn(f, buf) 768 require.EqualErrno(t, experimentalsys.ENOSYS, errno) 769 }) 770 } 771 } 772 773 func TestFileWrite_Errors(t *testing.T) { 774 // Create the file 775 path := path.Join(t.TempDir(), emptyFile) 776 of, err := os.Create(path) 777 require.NoError(t, err) 778 require.NoError(t, of.Close()) 779 780 // Open the file read-only 781 flag := experimentalsys.O_RDONLY 782 f := requireOpenFile(t, path, flag, 0o600) 783 defer f.Close() 784 buf := []byte("wazero") 785 786 tests := []struct { 787 name string 788 fn func(experimentalsys.File) experimentalsys.Errno 789 }{ 790 {name: "Write", fn: func(f experimentalsys.File) experimentalsys.Errno { 791 _, errno := f.Write(buf) 792 return errno 793 }}, 794 {name: "Pwrite", fn: func(f experimentalsys.File) experimentalsys.Errno { 795 _, errno := f.Pwrite(buf, 0) 796 return errno 797 }}, 798 } 799 800 for _, tc := range tests { 801 tc := tc 802 803 t.Run(tc.name, func(t *testing.T) { 804 t.Run("EBADF when not open for writing", func(t *testing.T) { 805 // The descriptor exists, but not open for writing 806 errno := tc.fn(f) 807 require.EqualErrno(t, experimentalsys.EBADF, errno) 808 }) 809 testEISDIR(t, tc.fn) 810 }) 811 } 812 } 813 814 func TestFileSync_NoError(t *testing.T) { 815 testSync_NoError(t, experimentalsys.File.Sync) 816 } 817 818 func TestFileDatasync_NoError(t *testing.T) { 819 testSync_NoError(t, experimentalsys.File.Datasync) 820 } 821 822 func testSync_NoError(t *testing.T, sync func(experimentalsys.File) experimentalsys.Errno) { 823 roPath := "file_test.go" 824 ro, errno := OpenFSFile(embedFS, roPath, experimentalsys.O_RDONLY, 0) 825 require.EqualErrno(t, 0, errno) 826 defer ro.Close() 827 828 rwPath := path.Join(t.TempDir(), "datasync") 829 rw, errno := OpenOSFile(rwPath, experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600) 830 require.EqualErrno(t, 0, errno) 831 defer rw.Close() 832 833 tests := []struct { 834 name string 835 f experimentalsys.File 836 }{ 837 {name: "UnimplementedFile", f: experimentalsys.UnimplementedFile{}}, 838 {name: "File of read-only FS.File", f: ro}, 839 {name: "File of os.File", f: rw}, 840 } 841 842 for _, tt := range tests { 843 tc := tt 844 845 t.Run(tc.name, func(t *testing.T) { 846 require.EqualErrno(t, 0, sync(tc.f)) 847 }) 848 } 849 } 850 851 func TestFileSync(t *testing.T) { 852 testSync(t, experimentalsys.File.Sync) 853 } 854 855 func TestFileDatasync(t *testing.T) { 856 testSync(t, experimentalsys.File.Datasync) 857 } 858 859 // testSync doesn't guarantee sync works because the operating system may 860 // sync anyway. There is no test in Go for syscall.Fdatasync, but closest is 861 // similar to below. Effectively, this only tests that things don't error. 862 func testSync(t *testing.T, sync func(experimentalsys.File) experimentalsys.Errno) { 863 // Even though it is invalid, try to sync a directory 864 dPath := t.TempDir() 865 d := requireOpenFile(t, dPath, experimentalsys.O_RDONLY, 0) 866 defer d.Close() 867 868 errno := sync(d) 869 require.EqualErrno(t, 0, errno) 870 871 fPath := path.Join(dPath, t.Name()) 872 873 f := requireOpenFile(t, fPath, experimentalsys.O_RDWR|experimentalsys.O_CREAT, 0o600) 874 defer f.Close() 875 876 expected := "hello world!" 877 878 // Write the expected data 879 _, errno = f.Write([]byte(expected)) 880 require.EqualErrno(t, 0, errno) 881 882 // Sync the data. 883 errno = sync(f) 884 require.EqualErrno(t, 0, errno) 885 886 // Rewind while the file is still open. 887 _, errno = f.Seek(0, io.SeekStart) 888 require.EqualErrno(t, 0, errno) 889 890 // Read data from the file 891 buf := make([]byte, 50) 892 n, errno := f.Read(buf) 893 require.EqualErrno(t, 0, errno) 894 895 // It may be the case that sync worked. 896 require.Equal(t, expected, string(buf[:n])) 897 898 // Windows allows you to sync a closed file 899 if runtime.GOOS != "windows" { 900 testEBADFIfFileClosed(t, sync) 901 testEBADFIfDirClosed(t, sync) 902 } 903 } 904 905 func TestFileTruncate(t *testing.T) { 906 content := []byte("123456") 907 908 tests := []struct { 909 name string 910 size int64 911 expectedContent []byte 912 expectedErr error 913 }{ 914 { 915 name: "one less", 916 size: 5, 917 expectedContent: []byte("12345"), 918 }, 919 { 920 name: "same", 921 size: 6, 922 expectedContent: content, 923 }, 924 { 925 name: "zero", 926 size: 0, 927 expectedContent: []byte(""), 928 }, 929 { 930 name: "larger", 931 size: 106, 932 expectedContent: append(content, make([]byte, 100)...), 933 }, 934 } 935 936 for _, tt := range tests { 937 tc := tt 938 t.Run(tc.name, func(t *testing.T) { 939 tmpDir := t.TempDir() 940 941 fPath := path.Join(tmpDir, tc.name) 942 f := openForWrite(t, fPath, content) 943 defer f.Close() 944 945 errno := f.Truncate(tc.size) 946 require.EqualErrno(t, 0, errno) 947 948 actual, err := os.ReadFile(fPath) 949 require.NoError(t, err) 950 require.Equal(t, tc.expectedContent, actual) 951 }) 952 } 953 954 truncateToZero := func(f experimentalsys.File) experimentalsys.Errno { 955 return f.Truncate(0) 956 } 957 958 if runtime.GOOS != "windows" { 959 // TODO: os.Truncate on windows passes even when closed 960 testEBADFIfFileClosed(t, truncateToZero) 961 } 962 963 testEISDIR(t, truncateToZero) 964 965 t.Run("negative", func(t *testing.T) { 966 tmpDir := t.TempDir() 967 968 f := openForWrite(t, path.Join(tmpDir, "truncate"), content) 969 defer f.Close() 970 971 errno := f.Truncate(-1) 972 require.EqualErrno(t, experimentalsys.EINVAL, errno) 973 }) 974 } 975 976 func TestFileUtimens(t *testing.T) { 977 switch runtime.GOOS { 978 case "linux", "darwin": // supported 979 case "freebsd": // TODO: support freebsd w/o CGO 980 case "windows": 981 if !platform.IsAtLeastGo120 { 982 t.Skip("windows only works after Go 1.20") // TODO: possibly 1.19 ;) 983 } 984 default: // expect ENOSYS and callers need to fall back to Utimens 985 t.Skip("unsupported GOOS", runtime.GOOS) 986 } 987 988 testUtimens(t, true) 989 990 testEBADFIfFileClosed(t, func(f experimentalsys.File) experimentalsys.Errno { 991 return f.Utimens(experimentalsys.UTIME_OMIT, experimentalsys.UTIME_OMIT) 992 }) 993 testEBADFIfDirClosed(t, func(d experimentalsys.File) experimentalsys.Errno { 994 return d.Utimens(experimentalsys.UTIME_OMIT, experimentalsys.UTIME_OMIT) 995 }) 996 } 997 998 func TestNewStdioFile(t *testing.T) { 999 // simulate regular file attached to stdin 1000 f, err := os.CreateTemp(t.TempDir(), "somefile") 1001 require.NoError(t, err) 1002 defer f.Close() 1003 1004 stdin, err := NewStdioFile(true, os.Stdin) 1005 require.NoError(t, err) 1006 stdinStat, err := os.Stdin.Stat() 1007 require.NoError(t, err) 1008 1009 stdinFile, err := NewStdioFile(true, f) 1010 require.NoError(t, err) 1011 1012 stdout, err := NewStdioFile(false, os.Stdout) 1013 require.NoError(t, err) 1014 stdoutStat, err := os.Stdout.Stat() 1015 require.NoError(t, err) 1016 1017 stdoutFile, err := NewStdioFile(false, f) 1018 require.NoError(t, err) 1019 1020 tests := []struct { 1021 name string 1022 f experimentalsys.File 1023 // Depending on how the tests run, os.Stdin won't necessarily be a char 1024 // device. We compare against an os.File, to account for this. 1025 expectedType fs.FileMode 1026 }{ 1027 { 1028 name: "stdin", 1029 f: stdin, 1030 expectedType: stdinStat.Mode().Type(), 1031 }, 1032 { 1033 name: "stdin file", 1034 f: stdinFile, 1035 expectedType: 0, // normal file 1036 }, 1037 { 1038 name: "stdout", 1039 f: stdout, 1040 expectedType: stdoutStat.Mode().Type(), 1041 }, 1042 { 1043 name: "stdout file", 1044 f: stdoutFile, 1045 expectedType: 0, // normal file 1046 }, 1047 } 1048 1049 for _, tt := range tests { 1050 tc := tt 1051 1052 t.Run(tc.name+" Stat", func(t *testing.T) { 1053 st, errno := tc.f.Stat() 1054 require.EqualErrno(t, 0, errno) 1055 require.Equal(t, tc.expectedType, st.Mode&fs.ModeType) 1056 require.Equal(t, uint64(1), st.Nlink) 1057 1058 // Fake times are needed to pass wasi-testsuite. 1059 // See https://github.com/WebAssembly/wasi-testsuite/blob/af57727/tests/rust/src/bin/fd_filestat_get.rs#L1-L19 1060 require.Zero(t, st.Ctim) 1061 require.Zero(t, st.Mtim) 1062 require.Zero(t, st.Atim) 1063 }) 1064 } 1065 } 1066 1067 func testEBADFIfDirClosed(t *testing.T, fn func(experimentalsys.File) experimentalsys.Errno) bool { 1068 return t.Run("EBADF if dir closed", func(t *testing.T) { 1069 d := requireOpenFile(t, t.TempDir(), experimentalsys.O_RDONLY, 0o755) 1070 1071 // close the directory underneath 1072 require.EqualErrno(t, 0, d.Close()) 1073 1074 require.EqualErrno(t, experimentalsys.EBADF, fn(d)) 1075 }) 1076 } 1077 1078 func testEBADFIfFileClosed(t *testing.T, fn func(experimentalsys.File) experimentalsys.Errno) bool { 1079 return t.Run("EBADF if file closed", func(t *testing.T) { 1080 tmpDir := t.TempDir() 1081 1082 f := openForWrite(t, path.Join(tmpDir, "EBADF"), []byte{1, 2, 3, 4}) 1083 1084 // close the file underneath 1085 require.EqualErrno(t, 0, f.Close()) 1086 1087 require.EqualErrno(t, experimentalsys.EBADF, fn(f)) 1088 }) 1089 } 1090 1091 func testEISDIR(t *testing.T, fn func(experimentalsys.File) experimentalsys.Errno) bool { 1092 return t.Run("EISDIR if directory", func(t *testing.T) { 1093 f := requireOpenFile(t, os.TempDir(), experimentalsys.O_RDONLY|experimentalsys.O_DIRECTORY, 0o666) 1094 defer f.Close() 1095 1096 require.EqualErrno(t, experimentalsys.EISDIR, fn(f)) 1097 }) 1098 } 1099 1100 func openForWrite(t *testing.T, path string, content []byte) experimentalsys.File { 1101 require.NoError(t, os.WriteFile(path, content, 0o0666)) 1102 f := requireOpenFile(t, path, experimentalsys.O_RDWR, 0o666) 1103 _, errno := f.Write(content) 1104 require.EqualErrno(t, 0, errno) 1105 return f 1106 } 1107 1108 func requireOpenFile(t *testing.T, path string, flag experimentalsys.Oflag, perm fs.FileMode) experimentalsys.File { 1109 f, errno := OpenOSFile(path, flag, perm) 1110 require.EqualErrno(t, 0, errno) 1111 return f 1112 } 1113 1114 func dirEmbedMapFS(t *testing.T, tmpDir string) (fs.FS, fs.FS, fs.FS) { 1115 embedFS, err := fs.Sub(testdata, "testdata") 1116 require.NoError(t, err) 1117 1118 f, err := embedFS.Open(wazeroFile) 1119 require.NoError(t, err) 1120 defer f.Close() 1121 1122 bytes, err := io.ReadAll(f) 1123 require.NoError(t, err) 1124 1125 mapFS := gofstest.MapFS{ 1126 emptyFile: &gofstest.MapFile{}, 1127 wazeroFile: &gofstest.MapFile{Data: bytes}, 1128 } 1129 1130 // Write a file as can't open "testdata" in scratch tests because they 1131 // can't read the original filesystem. 1132 require.NoError(t, os.WriteFile(path.Join(tmpDir, emptyFile), nil, 0o600)) 1133 require.NoError(t, os.WriteFile(path.Join(tmpDir, wazeroFile), bytes, 0o600)) 1134 dirFS := os.DirFS(tmpDir) 1135 return dirFS, embedFS, mapFS 1136 }