github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/internal/wasm/module_instance_test.go (about)

     1  package wasm
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/tetratelabs/wazero/experimental/sys"
    12  	internalsys "github.com/tetratelabs/wazero/internal/sys"
    13  	"github.com/tetratelabs/wazero/internal/sysfs"
    14  	testfs "github.com/tetratelabs/wazero/internal/testing/fs"
    15  	"github.com/tetratelabs/wazero/internal/testing/hammer"
    16  	"github.com/tetratelabs/wazero/internal/testing/require"
    17  )
    18  
    19  func TestModuleInstance_String(t *testing.T) {
    20  	s := newStore()
    21  
    22  	tests := []struct {
    23  		name, moduleName, expected string
    24  	}{
    25  		{
    26  			name:       "empty",
    27  			moduleName: "",
    28  			expected:   "Module[]",
    29  		},
    30  		{
    31  			name:       "not empty",
    32  			moduleName: "math",
    33  			expected:   "Module[math]",
    34  		},
    35  	}
    36  
    37  	for _, tt := range tests {
    38  		tc := tt
    39  
    40  		t.Run(tc.name, func(t *testing.T) {
    41  			// Ensure paths that can create the host module can see the name.
    42  			m, err := s.Instantiate(testCtx, &Module{}, tc.moduleName, nil, nil)
    43  			defer m.Close(testCtx) //nolint
    44  
    45  			require.NoError(t, err)
    46  			require.Equal(t, tc.expected, m.String())
    47  
    48  			if name := m.Name(); name != "" {
    49  				sm := s.Module(m.Name())
    50  				if sm != nil {
    51  					require.Equal(t, tc.expected, s.Module(m.Name()).String())
    52  				} else {
    53  					require.Zero(t, len(m.Name()))
    54  				}
    55  			}
    56  		})
    57  	}
    58  }
    59  
    60  func TestModuleInstance_Close(t *testing.T) {
    61  	s := newStore()
    62  
    63  	tests := []struct {
    64  		name           string
    65  		closer         func(context.Context, *ModuleInstance) error
    66  		expectedClosed uint64
    67  	}{
    68  		{
    69  			name: "Close()",
    70  			closer: func(ctx context.Context, m *ModuleInstance) error {
    71  				return m.Close(ctx)
    72  			},
    73  			expectedClosed: uint64(1),
    74  		},
    75  		{
    76  			name: "CloseWithExitCode(255)",
    77  			closer: func(ctx context.Context, m *ModuleInstance) error {
    78  				return m.CloseWithExitCode(ctx, 255)
    79  			},
    80  			expectedClosed: uint64(255)<<32 + 1,
    81  		},
    82  	}
    83  
    84  	for _, tt := range tests {
    85  		tc := tt
    86  		t.Run(fmt.Sprintf("%s calls ns.CloseWithExitCode(module.name))", tc.name), func(t *testing.T) {
    87  			moduleName := t.Name()
    88  			m, err := s.Instantiate(testCtx, &Module{}, moduleName, nil, nil)
    89  			require.NoError(t, err)
    90  
    91  			// We use side effects to see if Close called ns.CloseWithExitCode (without repeating store_test.go).
    92  			// One side effect of ns.CloseWithExitCode is that the moduleName can no longer be looked up.
    93  			require.Equal(t, s.Module(moduleName), m)
    94  
    95  			// Closing should not err.
    96  			require.NoError(t, tc.closer(testCtx, m))
    97  
    98  			require.Equal(t, tc.expectedClosed, m.Closed.Load())
    99  
   100  			// Outside callers should be able to know it was closed.
   101  			require.True(t, m.IsClosed())
   102  
   103  			// Verify our intended side-effect
   104  			require.Nil(t, s.Module(moduleName))
   105  
   106  			// Verify no error closing again.
   107  			require.NoError(t, tc.closer(testCtx, m))
   108  		})
   109  	}
   110  
   111  	t.Run("calls Context.Close()", func(t *testing.T) {
   112  		testFS := &sysfs.AdaptFS{FS: testfs.FS{"foo": &testfs.File{}}}
   113  		sysCtx := internalsys.DefaultContext(testFS)
   114  		fsCtx := sysCtx.FS()
   115  
   116  		_, errno := fsCtx.OpenFile(testFS, "/foo", sys.O_RDONLY, 0)
   117  		require.EqualErrno(t, 0, errno)
   118  
   119  		m, err := s.Instantiate(testCtx, &Module{}, t.Name(), sysCtx, nil)
   120  		require.NoError(t, err)
   121  
   122  		// We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go).
   123  		// One side effect of Context.Close is that it clears the openedFiles map. Verify our base case.
   124  		_, ok := fsCtx.LookupFile(3)
   125  		require.True(t, ok, "sysCtx.openedFiles was empty")
   126  
   127  		// Closing should not err even when concurrently closed.
   128  		hammer.NewHammer(t, 100, 10).Run(func(p, n int) {
   129  			require.NoError(t, m.Close(testCtx))
   130  			// closeWithExitCode is the one called during Store.CloseWithExitCode.
   131  			require.NoError(t, m.closeWithExitCode(testCtx, 0))
   132  		}, nil)
   133  		if t.Failed() {
   134  			return // At least one test failed, so return now.
   135  		}
   136  
   137  		// Verify our intended side-effect
   138  		_, ok = fsCtx.LookupFile(3)
   139  		require.False(t, ok, "expected no opened files")
   140  
   141  		// Verify no error closing again.
   142  		require.NoError(t, m.Close(testCtx))
   143  	})
   144  
   145  	t.Run("error closing", func(t *testing.T) {
   146  		// Right now, the only way to err closing the sys context is if a File.Close erred.
   147  		testFS := &sysfs.AdaptFS{FS: testfs.FS{"foo": &testfs.File{CloseErr: errors.New("error closing")}}}
   148  		sysCtx := internalsys.DefaultContext(testFS)
   149  		fsCtx := sysCtx.FS()
   150  
   151  		_, errno := fsCtx.OpenFile(testFS, "/foo", sys.O_RDONLY, 0)
   152  		require.EqualErrno(t, 0, errno)
   153  
   154  		m, err := s.Instantiate(testCtx, &Module{}, t.Name(), sysCtx, nil)
   155  		require.NoError(t, err)
   156  
   157  		// In sys.FS, non syscall errors map to sys.EIO.
   158  		require.EqualErrno(t, sys.EIO, m.Close(testCtx))
   159  
   160  		// Verify our intended side-effect
   161  		_, ok := fsCtx.LookupFile(3)
   162  		require.False(t, ok, "expected no opened files")
   163  	})
   164  }
   165  
   166  func TestModuleInstance_CallDynamic(t *testing.T) {
   167  	s := newStore()
   168  
   169  	tests := []struct {
   170  		name           string
   171  		closer         func(context.Context, *ModuleInstance) error
   172  		expectedClosed uint64
   173  	}{
   174  		{
   175  			name: "Close()",
   176  			closer: func(ctx context.Context, m *ModuleInstance) error {
   177  				return m.Close(ctx)
   178  			},
   179  			expectedClosed: uint64(1),
   180  		},
   181  		{
   182  			name: "CloseWithExitCode(255)",
   183  			closer: func(ctx context.Context, m *ModuleInstance) error {
   184  				return m.CloseWithExitCode(ctx, 255)
   185  			},
   186  			expectedClosed: uint64(255)<<32 + 1,
   187  		},
   188  	}
   189  
   190  	for _, tt := range tests {
   191  		tc := tt
   192  		t.Run(fmt.Sprintf("%s calls ns.CloseWithExitCode(module.name))", tc.name), func(t *testing.T) {
   193  			moduleName := t.Name()
   194  			m, err := s.Instantiate(testCtx, &Module{}, moduleName, nil, nil)
   195  			require.NoError(t, err)
   196  
   197  			// We use side effects to see if Close called ns.CloseWithExitCode (without repeating store_test.go).
   198  			// One side effect of ns.CloseWithExitCode is that the moduleName can no longer be looked up.
   199  			require.Equal(t, s.Module(moduleName), m)
   200  
   201  			// Closing should not err.
   202  			require.NoError(t, tc.closer(testCtx, m))
   203  
   204  			require.Equal(t, tc.expectedClosed, m.Closed.Load())
   205  
   206  			// Verify our intended side-effect
   207  			require.Nil(t, s.Module(moduleName))
   208  
   209  			// Verify no error closing again.
   210  			require.NoError(t, tc.closer(testCtx, m))
   211  		})
   212  	}
   213  
   214  	t.Run("calls Context.Close()", func(t *testing.T) {
   215  		testFS := &sysfs.AdaptFS{FS: testfs.FS{"foo": &testfs.File{}}}
   216  		sysCtx := internalsys.DefaultContext(testFS)
   217  		fsCtx := sysCtx.FS()
   218  
   219  		_, errno := fsCtx.OpenFile(testFS, "/foo", sys.O_RDONLY, 0)
   220  		require.EqualErrno(t, 0, errno)
   221  
   222  		m, err := s.Instantiate(testCtx, &Module{}, t.Name(), sysCtx, nil)
   223  		require.NoError(t, err)
   224  
   225  		// We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go).
   226  		// One side effect of Context.Close is that it clears the openedFiles map. Verify our base case.
   227  		_, ok := fsCtx.LookupFile(3)
   228  		require.True(t, ok, "sysCtx.openedFiles was empty")
   229  
   230  		// Closing should not err.
   231  		require.NoError(t, m.Close(testCtx))
   232  
   233  		// Verify our intended side-effect
   234  		_, ok = fsCtx.LookupFile(3)
   235  		require.False(t, ok, "expected no opened files")
   236  
   237  		// Verify no error closing again.
   238  		require.NoError(t, m.Close(testCtx))
   239  	})
   240  
   241  	t.Run("error closing", func(t *testing.T) {
   242  		// Right now, the only way to err closing the sys context is if a File.Close erred.
   243  		testFS := &sysfs.AdaptFS{FS: testfs.FS{"foo": &testfs.File{CloseErr: errors.New("error closing")}}}
   244  		sysCtx := internalsys.DefaultContext(testFS)
   245  		fsCtx := sysCtx.FS()
   246  
   247  		path := "/foo"
   248  		_, errno := fsCtx.OpenFile(testFS, path, sys.O_RDONLY, 0)
   249  		require.EqualErrno(t, 0, errno)
   250  
   251  		m, err := s.Instantiate(testCtx, &Module{}, t.Name(), sysCtx, nil)
   252  		require.NoError(t, err)
   253  
   254  		// In sys.FS, non syscall errors map to sys.EIO.
   255  		require.EqualErrno(t, sys.EIO, m.Close(testCtx))
   256  
   257  		// Verify our intended side-effect
   258  		_, ok := fsCtx.LookupFile(3)
   259  		require.False(t, ok, "expected no opened files")
   260  	})
   261  }
   262  
   263  func TestModuleInstance_CloseModuleOnCanceledOrTimeout(t *testing.T) {
   264  	s := newStore()
   265  	t.Run("timeout", func(t *testing.T) {
   266  		cc := &ModuleInstance{ModuleName: "test", s: s, Sys: internalsys.DefaultContext(nil)}
   267  		const duration = time.Second
   268  		ctx, cancel := context.WithTimeout(context.Background(), duration)
   269  		defer cancel()
   270  		done := cc.CloseModuleOnCanceledOrTimeout(context.WithValue(ctx, struct{}{}, 1)) // Wrapping arbitrary context.
   271  		time.Sleep(duration * 2)
   272  		defer done()
   273  
   274  		// Resource shouldn't be released at this point.
   275  		require.Equal(t, exitCodeFlag(exitCodeFlagResourceNotClosed), cc.Closed.Load()&exitCodeFlagMask)
   276  		require.NotNil(t, cc.Sys)
   277  
   278  		err := cc.FailIfClosed()
   279  		require.EqualError(t, err, "module closed with context deadline exceeded")
   280  
   281  		// The resource must be closed in FailIfClosed.
   282  		require.Nil(t, cc.Sys)
   283  	})
   284  
   285  	t.Run("cancel", func(t *testing.T) {
   286  		cc := &ModuleInstance{ModuleName: "test", s: s, Sys: internalsys.DefaultContext(nil)}
   287  		ctx, cancel := context.WithCancel(context.Background())
   288  		done := cc.CloseModuleOnCanceledOrTimeout(context.WithValue(ctx, struct{}{}, 1)) // Wrapping arbitrary context.
   289  		cancel()
   290  		// Make sure nothing panics or otherwise gets weird with redundant call to cancel().
   291  		cancel()
   292  		cancel()
   293  		defer done()
   294  		time.Sleep(time.Second)
   295  
   296  		// Resource shouldn't be released at this point.
   297  		require.Equal(t, exitCodeFlag(exitCodeFlagResourceNotClosed), cc.Closed.Load()&exitCodeFlagMask)
   298  		require.NotNil(t, cc.Sys)
   299  
   300  		err := cc.FailIfClosed()
   301  		require.EqualError(t, err, "module closed with context canceled")
   302  
   303  		// The resource must be closed in FailIfClosed.
   304  		require.Nil(t, cc.Sys)
   305  	})
   306  
   307  	t.Run("timeout over cancel", func(t *testing.T) {
   308  		cc := &ModuleInstance{ModuleName: "test", s: s, Sys: internalsys.DefaultContext(nil)}
   309  		const duration = time.Second
   310  		ctx, cancel := context.WithCancel(context.Background())
   311  		defer cancel()
   312  		// Wrap the cancel context by timeout.
   313  		ctx, cancel = context.WithTimeout(ctx, duration)
   314  		defer cancel()
   315  		done := cc.CloseModuleOnCanceledOrTimeout(context.WithValue(ctx, struct{}{}, 1)) // Wrapping arbitrary context.
   316  		time.Sleep(duration * 2)
   317  		defer done()
   318  
   319  		// Resource shouldn't be released at this point.
   320  		require.Equal(t, exitCodeFlag(exitCodeFlagResourceNotClosed), cc.Closed.Load()&exitCodeFlagMask)
   321  		require.NotNil(t, cc.Sys)
   322  
   323  		err := cc.FailIfClosed()
   324  		require.EqualError(t, err, "module closed with context deadline exceeded")
   325  
   326  		// The resource must be closed in FailIfClosed.
   327  		require.Nil(t, cc.Sys)
   328  	})
   329  
   330  	t.Run("cancel over timeout", func(t *testing.T) {
   331  		cc := &ModuleInstance{ModuleName: "test", s: s, Sys: internalsys.DefaultContext(nil)}
   332  		ctx, cancel := context.WithCancel(context.Background())
   333  		// Wrap the timeout context by cancel context.
   334  		var timeoutDone context.CancelFunc
   335  		ctx, timeoutDone = context.WithTimeout(ctx, time.Second*1000)
   336  		defer timeoutDone()
   337  
   338  		done := cc.CloseModuleOnCanceledOrTimeout(context.WithValue(ctx, struct{}{}, 1)) // Wrapping arbitrary context.
   339  		cancel()
   340  		defer done()
   341  
   342  		time.Sleep(time.Second)
   343  
   344  		// Resource shouldn't be released at this point.
   345  		require.Equal(t, exitCodeFlag(exitCodeFlagResourceNotClosed), cc.Closed.Load()&exitCodeFlagMask)
   346  		require.NotNil(t, cc.Sys)
   347  
   348  		err := cc.FailIfClosed()
   349  		require.EqualError(t, err, "module closed with context canceled")
   350  
   351  		// The resource must be closed in FailIfClosed.
   352  		require.Nil(t, cc.Sys)
   353  	})
   354  
   355  	t.Run("cancel works", func(t *testing.T) {
   356  		cc := &ModuleInstance{ModuleName: "test", s: s}
   357  		cancelChan := make(chan struct{})
   358  		var wg sync.WaitGroup
   359  		wg.Add(1)
   360  
   361  		// Ensure that fn returned by closeModuleOnCanceledOrTimeout exists after cancelFn is called.
   362  		go func() {
   363  			defer wg.Done()
   364  			cc.closeModuleOnCanceledOrTimeout(context.Background(), cancelChan)
   365  		}()
   366  		close(cancelChan)
   367  		wg.Wait()
   368  	})
   369  
   370  	t.Run("no close on all resources canceled", func(t *testing.T) {
   371  		cc := &ModuleInstance{ModuleName: "test", s: s}
   372  		cancelChan := make(chan struct{})
   373  		close(cancelChan)
   374  		ctx, cancel := context.WithCancel(context.Background())
   375  		cancel()
   376  
   377  		cc.closeModuleOnCanceledOrTimeout(ctx, cancelChan)
   378  
   379  		err := cc.FailIfClosed()
   380  		require.Nil(t, err)
   381  	})
   382  }
   383  
   384  func TestModuleInstance_CloseWithCtxErr(t *testing.T) {
   385  	s := newStore()
   386  
   387  	t.Run("context canceled", func(t *testing.T) {
   388  		cc := &ModuleInstance{ModuleName: "test", s: s}
   389  		ctx, cancel := context.WithCancel(context.Background())
   390  		cancel()
   391  
   392  		cc.CloseWithCtxErr(ctx)
   393  
   394  		err := cc.FailIfClosed()
   395  		require.EqualError(t, err, "module closed with context canceled")
   396  	})
   397  
   398  	t.Run("context timeout", func(t *testing.T) {
   399  		cc := &ModuleInstance{ModuleName: "test", s: s}
   400  		duration := time.Second
   401  		ctx, cancel := context.WithTimeout(context.Background(), duration)
   402  		defer cancel()
   403  
   404  		time.Sleep(duration * 2)
   405  
   406  		cc.CloseWithCtxErr(ctx)
   407  
   408  		err := cc.FailIfClosed()
   409  		require.EqualError(t, err, "module closed with context deadline exceeded")
   410  	})
   411  
   412  	t.Run("no error", func(t *testing.T) {
   413  		cc := &ModuleInstance{ModuleName: "test", s: s}
   414  
   415  		cc.CloseWithCtxErr(context.Background())
   416  
   417  		err := cc.FailIfClosed()
   418  		require.Nil(t, err)
   419  	})
   420  }
   421  
   422  type mockCloser struct{ called int }
   423  
   424  func (m *mockCloser) Close(context.Context) error {
   425  	m.called++
   426  	return nil
   427  }
   428  
   429  func TestModuleInstance_ensureResourcesClosed(t *testing.T) {
   430  	closer := &mockCloser{}
   431  
   432  	for _, tc := range []struct {
   433  		name string
   434  		m    *ModuleInstance
   435  	}{
   436  		{m: &ModuleInstance{CodeCloser: closer}},
   437  		{m: &ModuleInstance{Sys: internalsys.DefaultContext(nil)}},
   438  		{m: &ModuleInstance{Sys: internalsys.DefaultContext(nil), CodeCloser: closer}},
   439  	} {
   440  		err := tc.m.ensureResourcesClosed(context.Background())
   441  		require.NoError(t, err)
   442  		require.Nil(t, tc.m.Sys)
   443  		require.Nil(t, tc.m.CodeCloser)
   444  
   445  		// Ensure multiple invocation is safe.
   446  		err = tc.m.ensureResourcesClosed(context.Background())
   447  		require.NoError(t, err)
   448  	}
   449  	require.Equal(t, 2, closer.called)
   450  }