github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/runtime_test.go (about) 1 package wazero 2 3 import ( 4 "context" 5 _ "embed" 6 "errors" 7 "sync" 8 "testing" 9 "time" 10 11 "github.com/wasilibs/wazerox/api" 12 "github.com/wasilibs/wazerox/experimental" 13 "github.com/wasilibs/wazerox/internal/filecache" 14 "github.com/wasilibs/wazerox/internal/leb128" 15 "github.com/wasilibs/wazerox/internal/platform" 16 "github.com/wasilibs/wazerox/internal/testing/binaryencoding" 17 "github.com/wasilibs/wazerox/internal/testing/require" 18 "github.com/wasilibs/wazerox/internal/wasm" 19 "github.com/wasilibs/wazerox/sys" 20 ) 21 22 var ( 23 binaryNamedZero = binaryencoding.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "0"}}) 24 // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. 25 testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") 26 ) 27 28 var _ context.Context = &HostContext{} 29 30 // HostContext contain the content will be used in host function call 31 type HostContext struct { 32 Content string 33 } 34 35 func (h *HostContext) Deadline() (deadline time.Time, ok bool) { return } 36 37 func (h *HostContext) Done() <-chan struct{} { return nil } 38 39 func (h *HostContext) Err() error { return nil } 40 41 func (h *HostContext) Value(key interface{}) interface{} { return nil } 42 43 func TestRuntime_CompileModule(t *testing.T) { 44 tests := []struct { 45 name string 46 runtime Runtime 47 wasm *wasm.Module 48 moduleBuilder HostModuleBuilder 49 expected func(CompiledModule) 50 }{ 51 { 52 name: "no name section", 53 wasm: &wasm.Module{}, 54 }, 55 { 56 name: "empty NameSection.ModuleName", 57 wasm: &wasm.Module{NameSection: &wasm.NameSection{}}, 58 }, 59 { 60 name: "NameSection.ModuleName", 61 wasm: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}}, 62 expected: func(compiled CompiledModule) { 63 require.Equal(t, "test", compiled.Name()) 64 }, 65 }, 66 { 67 name: "FunctionSection, but not exported", 68 wasm: &wasm.Module{ 69 TypeSection: []wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}}}, 70 FunctionSection: []wasm.Index{0}, 71 CodeSection: []wasm.Code{{Body: []byte{wasm.OpcodeEnd}}}, 72 }, 73 expected: func(compiled CompiledModule) { 74 require.Nil(t, compiled.ImportedFunctions()) 75 require.Zero(t, len(compiled.ExportedFunctions())) 76 }, 77 }, 78 { 79 name: "FunctionSection exported", 80 wasm: &wasm.Module{ 81 TypeSection: []wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}}}, 82 FunctionSection: []wasm.Index{0}, 83 CodeSection: []wasm.Code{{Body: []byte{wasm.OpcodeEnd}}}, 84 ExportSection: []wasm.Export{{ 85 Type: wasm.ExternTypeFunc, 86 Name: "function", 87 Index: 0, 88 }}, 89 }, 90 expected: func(compiled CompiledModule) { 91 require.Nil(t, compiled.ImportedFunctions()) 92 f := compiled.ExportedFunctions()["function"] 93 require.Equal(t, []api.ValueType{api.ValueTypeI32}, f.ParamTypes()) 94 }, 95 }, 96 { 97 name: "MemorySection, but not exported", 98 wasm: &wasm.Module{ 99 MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true}, 100 }, 101 expected: func(compiled CompiledModule) { 102 require.Nil(t, compiled.ImportedMemories()) 103 require.Zero(t, len(compiled.ExportedMemories())) 104 }, 105 }, 106 { 107 name: "MemorySection exported", 108 wasm: &wasm.Module{ 109 MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true}, 110 ExportSection: []wasm.Export{{ 111 Type: wasm.ExternTypeMemory, 112 Name: "memory", 113 Index: 0, 114 }}, 115 }, 116 expected: func(compiled CompiledModule) { 117 require.Nil(t, compiled.ImportedMemories()) 118 mem := compiled.ExportedMemories()["memory"] 119 require.Equal(t, uint32(2), mem.Min()) 120 max, ok := mem.Max() 121 require.Equal(t, uint32(3), max) 122 require.True(t, ok) 123 }, 124 }, 125 } 126 127 _r := NewRuntime(testCtx) 128 defer _r.Close(testCtx) 129 130 r := _r.(*runtime) 131 132 for _, tt := range tests { 133 tc := tt 134 135 t.Run(tc.name, func(t *testing.T) { 136 bin := binaryencoding.EncodeModule(tc.wasm) 137 138 m, err := r.CompileModule(testCtx, bin) 139 require.NoError(t, err) 140 if tc.expected == nil { 141 tc.expected = func(CompiledModule) {} 142 } 143 tc.expected(m) 144 require.Equal(t, r.store.Engine, m.(*compiledModule).compiledEngine) 145 146 // TypeIDs must be assigned to compiledModule. 147 expTypeIDs, err := r.store.GetFunctionTypeIDs(tc.wasm.TypeSection) 148 require.NoError(t, err) 149 require.Equal(t, expTypeIDs, m.(*compiledModule).typeIDs) 150 }) 151 } 152 } 153 154 func TestRuntime_CompileModule_Errors(t *testing.T) { 155 tests := []struct { 156 name string 157 wasm []byte 158 expectedErr string 159 }{ 160 { 161 name: "nil", 162 expectedErr: "invalid magic number", 163 }, 164 { 165 name: "invalid binary", 166 wasm: append(binaryencoding.Magic, []byte("yolo")...), 167 expectedErr: "invalid version header", 168 }, 169 { 170 name: "memory has too many pages", 171 wasm: binaryencoding.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: 70000, IsMaxEncoded: true}}), 172 expectedErr: "section memory: max 70000 pages (4 Gi) over limit of 65536 pages (4 Gi)", 173 }, 174 } 175 176 r := NewRuntime(testCtx) 177 defer r.Close(testCtx) 178 179 for _, tt := range tests { 180 tc := tt 181 182 t.Run(tc.name, func(t *testing.T) { 183 _, err := r.CompileModule(testCtx, tc.wasm) 184 require.EqualError(t, err, tc.expectedErr) 185 }) 186 } 187 } 188 189 // TestModule_Memory only covers a couple cases to avoid duplication of internal/wasm/runtime_test.go 190 func TestModule_Memory(t *testing.T) { 191 tests := []struct { 192 name string 193 wasm []byte 194 expected bool 195 expectedLen uint32 196 }{ 197 { 198 name: "no memory", 199 wasm: binaryencoding.EncodeModule(&wasm.Module{}), 200 }, 201 { 202 name: "memory exported, one page", 203 wasm: binaryencoding.EncodeModule(&wasm.Module{ 204 MemorySection: &wasm.Memory{Min: 1}, 205 ExportSection: []wasm.Export{{Name: "memory", Type: api.ExternTypeMemory}}, 206 }), 207 expected: true, 208 expectedLen: 65536, 209 }, 210 } 211 212 for _, tt := range tests { 213 tc := tt 214 215 t.Run(tc.name, func(t *testing.T) { 216 r := NewRuntime(testCtx) 217 defer r.Close(testCtx) 218 219 // Instantiate the module and get the export of the above memory 220 module, err := r.Instantiate(testCtx, tc.wasm) 221 require.NoError(t, err) 222 223 mem := module.ExportedMemory("memory") 224 if tc.expected { 225 require.Equal(t, tc.expectedLen, mem.Size()) 226 defs := module.ExportedMemoryDefinitions() 227 require.Equal(t, 1, len(defs)) 228 def := defs["memory"] 229 require.Equal(t, tc.expectedLen>>16, def.Min()) 230 } else { 231 require.Nil(t, mem) 232 require.Zero(t, len(module.ExportedMemoryDefinitions())) 233 } 234 }) 235 } 236 } 237 238 // TestModule_Global only covers a couple cases to avoid duplication of internal/wasm/global_test.go 239 func TestModule_Global(t *testing.T) { 240 globalVal := int64(100) // intentionally a value that differs in signed vs unsigned encoding 241 242 tests := []struct { 243 name string 244 module *wasm.Module // module as wat doesn't yet support globals 245 expected, expectedMutable bool 246 }{ 247 { 248 name: "no global", 249 module: &wasm.Module{}, 250 }, 251 { 252 name: "global not exported", 253 module: &wasm.Module{ 254 GlobalSection: []wasm.Global{ 255 { 256 Type: wasm.GlobalType{ValType: wasm.ValueTypeI64, Mutable: true}, 257 Init: wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(globalVal)}, 258 }, 259 }, 260 }, 261 }, 262 { 263 name: "global exported", 264 module: &wasm.Module{ 265 GlobalSection: []wasm.Global{ 266 { 267 Type: wasm.GlobalType{ValType: wasm.ValueTypeI64}, 268 Init: wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(globalVal)}, 269 }, 270 }, 271 Exports: map[string]*wasm.Export{ 272 "global": {Type: wasm.ExternTypeGlobal, Name: "global"}, 273 }, 274 }, 275 expected: true, 276 }, 277 { 278 name: "global exported and mutable", 279 module: &wasm.Module{ 280 GlobalSection: []wasm.Global{ 281 { 282 Type: wasm.GlobalType{ValType: wasm.ValueTypeI64, Mutable: true}, 283 Init: wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt64(globalVal)}, 284 }, 285 }, 286 Exports: map[string]*wasm.Export{ 287 "global": {Type: wasm.ExternTypeGlobal, Name: "global"}, 288 }, 289 }, 290 expected: true, 291 expectedMutable: true, 292 }, 293 } 294 295 for _, tt := range tests { 296 tc := tt 297 298 t.Run(tc.name, func(t *testing.T) { 299 r := NewRuntime(testCtx).(*runtime) 300 defer r.Close(testCtx) 301 302 code := &compiledModule{module: tc.module} 303 304 err := r.store.Engine.CompileModule(testCtx, code.module, nil, false) 305 require.NoError(t, err) 306 307 // Instantiate the module and get the export of the above global 308 module, err := r.InstantiateModule(testCtx, code, NewModuleConfig()) 309 require.NoError(t, err) 310 311 global := module.ExportedGlobal("global") 312 if !tc.expected { 313 require.Nil(t, global) 314 return 315 } 316 require.Equal(t, uint64(globalVal), global.Get()) 317 318 mutable, ok := global.(api.MutableGlobal) 319 require.Equal(t, tc.expectedMutable, ok) 320 if ok { 321 mutable.Set(2) 322 require.Equal(t, uint64(2), global.Get()) 323 } 324 }) 325 } 326 } 327 328 func TestRuntime_InstantiateModule_UsesContext(t *testing.T) { 329 r := NewRuntime(testCtx) 330 defer r.Close(testCtx) 331 332 // Define a function that will be set as the start function 333 var calledStart bool 334 start := func(ctx context.Context) { 335 calledStart = true 336 require.Equal(t, testCtx, ctx) 337 } 338 339 _, err := r.NewHostModuleBuilder("env"). 340 NewFunctionBuilder().WithFunc(start).Export("start"). 341 Instantiate(testCtx) 342 require.NoError(t, err) 343 344 one := uint32(1) 345 binary := binaryencoding.EncodeModule(&wasm.Module{ 346 TypeSection: []wasm.FunctionType{{}}, 347 ImportSection: []wasm.Import{{Module: "env", Name: "start", Type: wasm.ExternTypeFunc, DescFunc: 0}}, 348 FunctionSection: []wasm.Index{0}, 349 CodeSection: []wasm.Code{ 350 {Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.start. 351 }, 352 StartSection: &one, 353 }) 354 355 code, err := r.CompileModule(testCtx, binary) 356 require.NoError(t, err) 357 358 // Instantiate the module, which calls the start function. This will fail if the context wasn't as intended. 359 mod, err := r.InstantiateModule(testCtx, code, NewModuleConfig()) 360 require.NoError(t, err) 361 362 require.True(t, calledStart) 363 364 // Closing the module shouldn't remove the compiler cache 365 require.NoError(t, mod.Close(testCtx)) 366 require.Equal(t, uint32(2), r.(*runtime).store.Engine.CompiledModuleCount()) 367 } 368 369 // TestRuntime_Instantiate_DoesntEnforce_Start ensures wapc-go work when modules import WASI, but don't 370 // export "_start". 371 func TestRuntime_Instantiate_DoesntEnforce_Start(t *testing.T) { 372 r := NewRuntime(testCtx) 373 defer r.Close(testCtx) 374 375 binary := binaryencoding.EncodeModule(&wasm.Module{ 376 MemorySection: &wasm.Memory{Min: 1}, 377 ExportSection: []wasm.Export{{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0}}, 378 }) 379 380 mod, err := r.Instantiate(testCtx, binary) 381 require.NoError(t, err) 382 require.NoError(t, mod.Close(testCtx)) 383 } 384 385 func TestRuntime_Instantiate_ErrorOnStart(t *testing.T) { 386 tests := []struct { 387 name, wasm string 388 }{ 389 { 390 name: "_start function", 391 wasm: `(module 392 (import "" "start" (func $start)) 393 (export "_start" (func $start)) 394 )`, 395 }, 396 { 397 name: ".start function", 398 wasm: `(module 399 (import "" "start" (func $start)) 400 (start $start) 401 )`, 402 }, 403 } 404 405 for _, tt := range tests { 406 tc := tt 407 408 t.Run(tc.name, func(t *testing.T) { 409 r := NewRuntime(testCtx) 410 defer r.Close(testCtx) 411 412 start := func() { 413 panic(errors.New("ice cream")) 414 } 415 416 host, err := r.NewHostModuleBuilder("host"). 417 NewFunctionBuilder().WithFunc(start).Export("start"). 418 Instantiate(testCtx) 419 require.NoError(t, err) 420 421 // Start the module as a WASI command. We expect it to fail. 422 _, err = r.Instantiate(testCtx, []byte(tc.wasm)) 423 require.Error(t, err) 424 425 // Close the imported module, which should remove its compiler cache. 426 require.NoError(t, host.Close(testCtx)) 427 428 // The compiler cache of the importing module should be removed on error. 429 require.Zero(t, r.(*runtime).store.Engine.CompiledModuleCount()) 430 }) 431 } 432 } 433 434 // TestRuntime_InstantiateModule_WithName tests that we can pre-validate (cache) a module and instantiate it under 435 // different names. This pattern is used in wapc-go. 436 func TestRuntime_InstantiateModule_WithName(t *testing.T) { 437 r := NewRuntime(testCtx) 438 defer r.Close(testCtx) 439 440 base, err := r.CompileModule(testCtx, binaryNamedZero) 441 require.NoError(t, err) 442 443 require.Equal(t, "0", base.(*compiledModule).module.NameSection.ModuleName) 444 445 // Use the same runtime to instantiate multiple modules 446 internal := r.(*runtime) 447 m1, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("1")) 448 require.NoError(t, err) 449 require.Equal(t, "1", m1.Name()) 450 451 require.Nil(t, internal.Module("0")) 452 require.Equal(t, internal.Module("1"), m1) 453 454 m2, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("2")) 455 require.NoError(t, err) 456 require.Equal(t, "2", m2.Name()) 457 458 require.Nil(t, internal.Module("0")) 459 require.Equal(t, internal.Module("2"), m2) 460 461 // Empty name module shouldn't be returned via Module() for future optimization. 462 m3, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("")) 463 require.NoError(t, err) 464 require.Equal(t, "", m3.Name()) 465 466 ret := internal.Module("") 467 require.Nil(t, ret) 468 } 469 470 func TestRuntime_InstantiateModule_ExitError(t *testing.T) { 471 r := NewRuntime(testCtx) 472 defer r.Close(testCtx) 473 474 tests := []struct { 475 name string 476 exitCode uint32 477 export bool 478 expectedErr error 479 }{ 480 { 481 name: "start: exit code 0", 482 exitCode: 0, 483 expectedErr: sys.NewExitError(0), 484 }, 485 { 486 name: "start: exit code 2", 487 exitCode: 2, 488 expectedErr: sys.NewExitError(2), 489 }, 490 { 491 name: "_start: exit code 0", 492 exitCode: 0, 493 export: true, 494 }, 495 { 496 name: "_start: exit code 2", 497 exitCode: 2, 498 export: true, 499 expectedErr: sys.NewExitError(2), 500 }, 501 } 502 503 for _, tt := range tests { 504 tc := tt 505 t.Run(tc.name, func(t *testing.T) { 506 start := func(ctx context.Context, m api.Module) { 507 require.NoError(t, m.CloseWithExitCode(ctx, tc.exitCode)) 508 } 509 510 env, err := r.NewHostModuleBuilder("env"). 511 NewFunctionBuilder().WithFunc(start).Export("exit"). 512 Instantiate(testCtx) 513 require.NoError(t, err) 514 defer env.Close(testCtx) 515 516 mod := &wasm.Module{ 517 TypeSection: []wasm.FunctionType{{}}, 518 ImportSection: []wasm.Import{{Module: "env", Name: "exit", Type: wasm.ExternTypeFunc, DescFunc: 0}}, 519 FunctionSection: []wasm.Index{0}, 520 CodeSection: []wasm.Code{ 521 {Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.start. 522 }, 523 } 524 if tc.export { 525 mod.ExportSection = []wasm.Export{ 526 {Name: "_start", Type: wasm.ExternTypeFunc, Index: 1}, 527 } 528 } else { 529 one := uint32(1) 530 mod.StartSection = &one 531 } 532 binary := binaryencoding.EncodeModule(mod) 533 534 // Instantiate the module, which calls the start function. 535 m, err := r.InstantiateWithConfig(testCtx, binary, 536 NewModuleConfig().WithName("call-exit")) 537 538 // Ensure the exit error propagated and didn't wrap. 539 require.Equal(t, tc.expectedErr, err) 540 541 // Ensure calling close again doesn't break 542 if err == nil { 543 require.NoError(t, m.Close(testCtx)) 544 } 545 }) 546 } 547 } 548 549 func TestRuntime_CloseWithExitCode(t *testing.T) { 550 bin := binaryencoding.EncodeModule(&wasm.Module{ 551 TypeSection: []wasm.FunctionType{{}}, 552 FunctionSection: []wasm.Index{0}, 553 CodeSection: []wasm.Code{{Body: []byte{wasm.OpcodeEnd}}}, 554 ExportSection: []wasm.Export{{Type: wasm.ExternTypeFunc, Index: 0, Name: "func"}}, 555 }) 556 557 tests := []struct { 558 name string 559 exitCode uint32 560 }{ 561 { 562 name: "exit code 0", 563 exitCode: uint32(0), 564 }, 565 { 566 name: "exit code 2", 567 exitCode: uint32(2), 568 }, 569 } 570 571 for _, tt := range tests { 572 tc := tt 573 t.Run(tc.name, func(t *testing.T) { 574 r := NewRuntime(testCtx) 575 576 code, err := r.CompileModule(testCtx, bin) 577 require.NoError(t, err) 578 579 // Instantiate two modules. 580 m1, err := r.InstantiateModule(testCtx, code, NewModuleConfig().WithName("mod1")) 581 require.NoError(t, err) 582 m2, err := r.InstantiateModule(testCtx, code, NewModuleConfig().WithName("mod2")) 583 require.NoError(t, err) 584 585 func1 := m1.ExportedFunction("func") 586 require.Equal(t, map[string]api.FunctionDefinition{"func": func1.Definition()}, 587 m1.ExportedFunctionDefinitions()) 588 func2 := m2.ExportedFunction("func") 589 require.Equal(t, map[string]api.FunctionDefinition{"func": func2.Definition()}, 590 m2.ExportedFunctionDefinitions()) 591 592 // Modules not closed so calls succeed 593 594 _, err = func1.Call(testCtx) 595 require.NoError(t, err) 596 597 _, err = func2.Call(testCtx) 598 require.NoError(t, err) 599 600 if tc.exitCode == 0 { 601 err = r.Close(testCtx) 602 } else { 603 err = r.CloseWithExitCode(testCtx, tc.exitCode) 604 } 605 require.NoError(t, err) 606 607 // Modules closed so calls fail 608 _, err = func1.Call(testCtx) 609 require.ErrorIs(t, err, sys.NewExitError(tc.exitCode)) 610 611 _, err = func2.Call(testCtx) 612 require.ErrorIs(t, err, sys.NewExitError(tc.exitCode)) 613 }) 614 } 615 } 616 617 func TestHostFunctionWithCustomContext(t *testing.T) { 618 for _, tc := range []struct { 619 name string 620 config RuntimeConfig 621 }{ 622 {name: "compiler", config: NewRuntimeConfigCompiler()}, 623 {name: "interpreter", config: NewRuntimeConfigInterpreter()}, 624 } { 625 t.Run(tc.name, func(t *testing.T) { 626 const fistString = "hello" 627 const secondString = "hello call" 628 hostCtx := &HostContext{fistString} 629 r := NewRuntimeWithConfig(hostCtx, tc.config) 630 defer r.Close(hostCtx) 631 632 // Define a function that will be set as the start function 633 var calledStart bool 634 var calledCall bool 635 start := func(ctx context.Context, module api.Module) { 636 hts, ok := ctx.(*HostContext) 637 if !ok { 638 t.Fatal("decorate call context could effect host ctx cast failed, please consider it.") 639 } 640 calledStart = true 641 require.NotNil(t, hts) 642 require.Equal(t, fistString, hts.Content) 643 } 644 645 callFunc := func(ctx context.Context, module api.Module) { 646 hts, ok := ctx.(*HostContext) 647 if !ok { 648 t.Fatal("decorate call context could effect host ctx cast failed, please consider it.") 649 } 650 calledCall = true 651 require.NotNil(t, hts) 652 require.Equal(t, secondString, hts.Content) 653 } 654 655 _, err := r.NewHostModuleBuilder("env"). 656 NewFunctionBuilder().WithFunc(start).Export("host"). 657 NewFunctionBuilder().WithFunc(callFunc).Export("host2"). 658 Instantiate(hostCtx) 659 require.NoError(t, err) 660 661 startFnIndex := uint32(2) 662 binary := binaryencoding.EncodeModule(&wasm.Module{ 663 TypeSection: []wasm.FunctionType{{}}, 664 ImportSection: []wasm.Import{ 665 {Module: "env", Name: "host", Type: wasm.ExternTypeFunc, DescFunc: 0}, 666 {Module: "env", Name: "host2", Type: wasm.ExternTypeFunc, DescFunc: 0}, 667 }, 668 FunctionSection: []wasm.Index{0, 0}, 669 CodeSection: []wasm.Code{ 670 {Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.host. 671 {Body: []byte{wasm.OpcodeCall, 1, wasm.OpcodeEnd}}, // Call the imported env.host. 672 }, 673 ExportSection: []wasm.Export{ 674 {Type: api.ExternTypeFunc, Name: "callHost", Index: uint32(3)}, 675 }, 676 StartSection: &startFnIndex, 677 }) 678 679 // Instantiate the module, which calls the start function. This will fail if the context wasn't as intended. 680 ins, err := r.Instantiate(hostCtx, binary) 681 require.NoError(t, err) 682 require.True(t, calledStart) 683 684 // add the new context content for call with used in host function 685 hostCtx.Content = secondString 686 _, err = ins.ExportedFunction("callHost").Call(hostCtx) 687 require.NoError(t, err) 688 require.True(t, calledCall) 689 }) 690 } 691 } 692 693 func TestRuntime_Close_ClosesCompiledModules(t *testing.T) { 694 for _, tc := range []struct { 695 name string 696 withCompilationCache bool 697 }{ 698 {name: "with cache", withCompilationCache: true}, 699 {name: "without cache", withCompilationCache: false}, 700 } { 701 t.Run(tc.name, func(t *testing.T) { 702 engine := &mockEngine{name: "mock", cachedModules: map[*wasm.Module]struct{}{}} 703 conf := *engineLessConfig 704 conf.newEngine = func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine { return engine } 705 if tc.withCompilationCache { 706 conf.cache = NewCompilationCache() 707 } 708 r := NewRuntimeWithConfig(testCtx, &conf) 709 defer r.Close(testCtx) 710 711 // Normally compiled modules are closed when instantiated but this is never instantiated. 712 _, err := r.CompileModule(testCtx, binaryNamedZero) 713 require.NoError(t, err) 714 require.Equal(t, uint32(1), engine.CompiledModuleCount()) 715 716 err = r.Close(testCtx) 717 require.NoError(t, err) 718 719 // Closing the runtime should remove the compiler cache if cache is not configured. 720 require.Equal(t, !tc.withCompilationCache, engine.closed) 721 }) 722 } 723 } 724 725 // TestRuntime_Closed ensures invocation of closed Runtime's methods is safe. 726 func TestRuntime_Closed(t *testing.T) { 727 for _, tc := range []struct { 728 name string 729 errFunc func(r Runtime, mod CompiledModule) error 730 }{ 731 { 732 name: "InstantiateModule", 733 errFunc: func(r Runtime, mod CompiledModule) error { 734 _, err := r.InstantiateModule(testCtx, mod, NewModuleConfig()) 735 return err 736 }, 737 }, 738 { 739 name: "Instantiate", 740 errFunc: func(r Runtime, mod CompiledModule) error { 741 _, err := r.Instantiate(testCtx, binaryNamedZero) 742 return err 743 }, 744 }, 745 { 746 name: "CompileModule", 747 errFunc: func(r Runtime, mod CompiledModule) error { 748 _, err := r.CompileModule(testCtx, binaryNamedZero) 749 return err 750 }, 751 }, 752 } { 753 t.Run(tc.name, func(t *testing.T) { 754 engine := &mockEngine{name: "mock", cachedModules: map[*wasm.Module]struct{}{}} 755 conf := *engineLessConfig 756 conf.newEngine = func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine { return engine } 757 r := NewRuntimeWithConfig(testCtx, &conf) 758 defer r.Close(testCtx) 759 760 // Normally compiled modules are closed when instantiated but this is never instantiated. 761 mod, err := r.CompileModule(testCtx, binaryNamedZero) 762 require.NoError(t, err) 763 require.Equal(t, uint32(1), engine.CompiledModuleCount()) 764 765 err = r.Close(testCtx) 766 require.NoError(t, err) 767 768 // Closing the runtime should remove the compiler cache if cache is not configured. 769 require.True(t, engine.closed) 770 771 require.EqualError(t, tc.errFunc(r, mod), "runtime closed with exit_code(0)") 772 }) 773 } 774 } 775 776 type mockEngine struct { 777 name string 778 cachedModules map[*wasm.Module]struct{} 779 closed bool 780 } 781 782 // CompileModule implements the same method as documented on wasm.Engine. 783 func (e *mockEngine) CompileModule(_ context.Context, module *wasm.Module, _ []experimental.FunctionListener, _ bool) error { 784 e.cachedModules[module] = struct{}{} 785 return nil 786 } 787 788 // CompiledModuleCount implements the same method as documented on wasm.Engine. 789 func (e *mockEngine) CompiledModuleCount() uint32 { 790 return uint32(len(e.cachedModules)) 791 } 792 793 // DeleteCompiledModule implements the same method as documented on wasm.Engine. 794 func (e *mockEngine) DeleteCompiledModule(module *wasm.Module) { 795 delete(e.cachedModules, module) 796 } 797 798 // NewModuleEngine implements the same method as documented on wasm.Engine. 799 func (e *mockEngine) NewModuleEngine(_ *wasm.Module, _ *wasm.ModuleInstance) (wasm.ModuleEngine, error) { 800 return nil, nil 801 } 802 803 // NewModuleEngine implements the same method as documented on wasm.Close. 804 func (e *mockEngine) Close() (err error) { 805 e.closed = true 806 return 807 } 808 809 // TestNewRuntime_concurrent ensures that concurrent execution of NewRuntime is race-free. 810 // This depends on -race flag. 811 func TestNewRuntime_concurrent(t *testing.T) { 812 const num = 100 813 var wg sync.WaitGroup 814 c := NewCompilationCache() 815 // If available, uses two engine configurations for the single compilation cache. 816 configs := [2]RuntimeConfig{NewRuntimeConfigInterpreter().WithCompilationCache(c)} 817 if platform.CompilerSupported() { 818 configs[1] = NewRuntimeConfigCompiler().WithCompilationCache(c) 819 } else { 820 configs[1] = NewRuntimeConfigInterpreter().WithCompilationCache(c) 821 } 822 wg.Add(num) 823 for i := 0; i < num; i++ { 824 i := i 825 go func() { 826 defer wg.Done() 827 r := NewRuntimeWithConfig(testCtx, configs[i%2]) 828 err := r.Close(testCtx) 829 require.NoError(t, err) 830 }() 831 } 832 wg.Wait() 833 }