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 }