github.com/tetratelabs/wazero@v1.2.1/internal/wasm/module_instance_test.go (about)

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