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