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 }