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  }