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 }