wa-lang.org/wazero@v1.0.2/internal/engine/compiler/engine_test.go (about) 1 package compiler 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "runtime" 9 "testing" 10 "unsafe" 11 12 "wa-lang.org/wazero/api" 13 "wa-lang.org/wazero/experimental" 14 "wa-lang.org/wazero/experimental/logging" 15 "wa-lang.org/wazero/internal/platform" 16 "wa-lang.org/wazero/internal/testing/enginetest" 17 "wa-lang.org/wazero/internal/testing/require" 18 "wa-lang.org/wazero/internal/wasm" 19 ) 20 21 // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. 22 var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") 23 24 var ( 25 // et is used for tests defined in the enginetest package. 26 et = &engineTester{} 27 functionLog bytes.Buffer 28 listenerFactory = logging.NewLoggingListenerFactory(&functionLog) 29 ) 30 31 // engineTester implements enginetest.EngineTester. 32 type engineTester struct{} 33 34 // IsCompiler implements the same method as documented on enginetest.EngineTester. 35 func (e *engineTester) IsCompiler() bool { 36 return true 37 } 38 39 // ListenerFactory implements the same method as documented on enginetest.EngineTester. 40 func (e *engineTester) ListenerFactory() experimental.FunctionListenerFactory { 41 return listenerFactory 42 } 43 44 // NewEngine implements the same method as documented on enginetest.EngineTester. 45 func (e *engineTester) NewEngine(enabledFeatures api.CoreFeatures) wasm.Engine { 46 return newEngine(context.Background(), enabledFeatures) 47 } 48 49 // CompiledFunctionPointerValue implements the same method as documented on enginetest.EngineTester. 50 func (e engineTester) CompiledFunctionPointerValue(me wasm.ModuleEngine, funcIndex wasm.Index) uint64 { 51 internal := me.(*moduleEngine) 52 return uint64(uintptr(unsafe.Pointer(internal.functions[funcIndex]))) 53 } 54 55 func TestCompiler_Engine_NewModuleEngine(t *testing.T) { 56 requireSupportedOSArch(t) 57 enginetest.RunTestEngine_NewModuleEngine(t, et) 58 } 59 60 func TestCompiler_Engine_InitializeFuncrefGlobals(t *testing.T) { 61 enginetest.RunTestEngine_InitializeFuncrefGlobals(t, et) 62 } 63 64 func TestCompiler_ModuleEngine_LookupFunction(t *testing.T) { 65 enginetest.RunTestModuleEngine_LookupFunction(t, et) 66 } 67 68 func TestCompiler_ModuleEngine_Call(t *testing.T) { 69 defer functionLog.Reset() 70 requireSupportedOSArch(t) 71 enginetest.RunTestModuleEngine_Call(t, et) 72 require.Equal(t, ` 73 --> .$0(1,2) 74 <-- (1,2) 75 `, "\n"+functionLog.String()) 76 } 77 78 func TestCompiler_ModuleEngine_Call_HostFn(t *testing.T) { 79 defer functionLog.Reset() 80 requireSupportedOSArch(t) 81 enginetest.RunTestModuleEngine_Call_HostFn(t, et) 82 } 83 84 func TestCompiler_ModuleEngine_Call_Errors(t *testing.T) { 85 defer functionLog.Reset() 86 requireSupportedOSArch(t) 87 enginetest.RunTestModuleEngine_Call_Errors(t, et) 88 89 // TODO: Currently, the listener doesn't get notified on errors as they are 90 // implemented with panic. This means the end hooks aren't make resulting 91 // in dangling logs like this: 92 // ==> host.host_div_by(4294967295) 93 // instead of seeing a return like 94 // <== DivByZero 95 require.Equal(t, ` 96 --> imported.div_by.wasm(1) 97 <-- (1) 98 --> imported.div_by.wasm(1) 99 <-- (1) 100 --> imported.div_by.wasm(0) 101 --> imported.div_by.wasm(1) 102 <-- (1) 103 --> imported.call->div_by.go(4294967295) 104 ==> host.div_by.go(4294967295) 105 --> imported.call->div_by.go(1) 106 ==> host.div_by.go(1) 107 <== (1) 108 <-- (1) 109 --> importing.call_import->call->div_by.go(0) 110 --> imported.call->div_by.go(0) 111 ==> host.div_by.go(0) 112 --> importing.call_import->call->div_by.go(1) 113 --> imported.call->div_by.go(1) 114 ==> host.div_by.go(1) 115 <== (1) 116 <-- (1) 117 <-- (1) 118 --> importing.call_import->call->div_by.go(4294967295) 119 --> imported.call->div_by.go(4294967295) 120 ==> host.div_by.go(4294967295) 121 --> importing.call_import->call->div_by.go(1) 122 --> imported.call->div_by.go(1) 123 ==> host.div_by.go(1) 124 <== (1) 125 <-- (1) 126 <-- (1) 127 --> importing.call_import->call->div_by.go(0) 128 --> imported.call->div_by.go(0) 129 ==> host.div_by.go(0) 130 --> importing.call_import->call->div_by.go(1) 131 --> imported.call->div_by.go(1) 132 ==> host.div_by.go(1) 133 <== (1) 134 <-- (1) 135 <-- (1) 136 `, "\n"+functionLog.String()) 137 } 138 139 func TestCompiler_ModuleEngine_Memory(t *testing.T) { 140 requireSupportedOSArch(t) 141 enginetest.RunTestModuleEngine_Memory(t, et) 142 } 143 144 // requireSupportedOSArch is duplicated also in the platform package to ensure no cyclic dependency. 145 func requireSupportedOSArch(t *testing.T) { 146 if !platform.CompilerSupported() { 147 t.Skip() 148 } 149 } 150 151 type fakeFinalizer map[*code]func(*code) 152 153 func (f fakeFinalizer) setFinalizer(obj interface{}, finalizer interface{}) { 154 cf := obj.(*code) 155 if _, ok := f[cf]; ok { // easier than adding a field for testing.T 156 panic(fmt.Sprintf("BUG: %v already had its finalizer set", cf)) 157 } 158 f[cf] = finalizer.(func(*code)) 159 } 160 161 func TestCompiler_CompileModule(t *testing.T) { 162 t.Run("ok", func(t *testing.T) { 163 e := et.NewEngine(api.CoreFeaturesV1).(*engine) 164 ff := fakeFinalizer{} 165 e.setFinalizer = ff.setFinalizer 166 167 okModule := &wasm.Module{ 168 TypeSection: []*wasm.FunctionType{{}}, 169 FunctionSection: []wasm.Index{0, 0, 0, 0}, 170 CodeSection: []*wasm.Code{ 171 {Body: []byte{wasm.OpcodeEnd}}, 172 {Body: []byte{wasm.OpcodeEnd}}, 173 {Body: []byte{wasm.OpcodeEnd}}, 174 {Body: []byte{wasm.OpcodeEnd}}, 175 }, 176 ID: wasm.ModuleID{}, 177 } 178 179 err := e.CompileModule(testCtx, okModule, nil) 180 require.NoError(t, err) 181 182 // Compiling same module shouldn't be compiled again, but instead should be cached. 183 err = e.CompileModule(testCtx, okModule, nil) 184 require.NoError(t, err) 185 186 compiled, ok := e.codes[okModule.ID] 187 require.True(t, ok) 188 require.Equal(t, len(okModule.FunctionSection), len(compiled)) 189 190 // Pretend the finalizer executed, by invoking them one-by-one. 191 for k, v := range ff { 192 v(k) 193 } 194 }) 195 196 t.Run("fail", func(t *testing.T) { 197 errModule := &wasm.Module{ 198 TypeSection: []*wasm.FunctionType{{}}, 199 FunctionSection: []wasm.Index{0, 0, 0}, 200 CodeSection: []*wasm.Code{ 201 {Body: []byte{wasm.OpcodeEnd}}, 202 {Body: []byte{wasm.OpcodeEnd}}, 203 {Body: []byte{wasm.OpcodeCall}}, // Call instruction without immediate for call target index is invalid and should fail to compile. 204 }, 205 ID: wasm.ModuleID{}, 206 } 207 errModule.BuildFunctionDefinitions() 208 209 e := et.NewEngine(api.CoreFeaturesV1).(*engine) 210 err := e.CompileModule(testCtx, errModule, nil) 211 require.EqualError(t, err, "failed to lower func[.$2] to wazeroir: handling instruction: apply stack failed for call: reading immediates: EOF") 212 213 // On the compilation failure, the compiled functions must not be cached. 214 _, ok := e.codes[errModule.ID] 215 require.False(t, ok) 216 }) 217 } 218 219 // TestCompiler_Releasecode_Panic tests that an unexpected panic has some identifying information in it. 220 func TestCompiler_Releasecode_Panic(t *testing.T) { 221 captured := require.CapturePanic(func() { 222 releaseCode(&code{ 223 indexInModule: 2, 224 sourceModule: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: t.Name()}}, 225 codeSegment: []byte{wasm.OpcodeEnd}, // never compiled means it was never mapped. 226 }) 227 }) 228 require.Contains(t, captured.Error(), fmt.Sprintf("compiler: failed to munmap code segment for %[1]s.function[2]", t.Name())) 229 } 230 231 // Ensures that value stack and call-frame stack are allocated on heap which 232 // allows us to safely access to their data region from native code. 233 // See comments on initialStackSize and initialCallFrameStackSize. 234 func TestCompiler_SliceAllocatedOnHeap(t *testing.T) { 235 enabledFeatures := api.CoreFeaturesV1 236 e := newEngine(context.Background(), enabledFeatures) 237 s, ns := wasm.NewStore(enabledFeatures, e) 238 239 const hostModuleName = "env" 240 const hostFnName = "grow_and_shrink_goroutine_stack" 241 hm, err := wasm.NewHostModule(hostModuleName, map[string]interface{}{hostFnName: func() { 242 // This function aggressively grow the goroutine stack by recursively 243 // calling the function many times. 244 callNum := 1000 245 var growGoroutineStack func() 246 growGoroutineStack = func() { 247 if callNum != 0 { 248 callNum-- 249 growGoroutineStack() 250 } 251 } 252 growGoroutineStack() 253 254 // Trigger relocation of goroutine stack because at this point we have the majority of 255 // goroutine stack unused after recursive call. 256 runtime.GC() 257 }}, nil, enabledFeatures) 258 require.NoError(t, err) 259 260 err = s.Engine.CompileModule(testCtx, hm, nil) 261 require.NoError(t, err) 262 263 _, err = s.Instantiate(testCtx, ns, hm, hostModuleName, nil) 264 require.NoError(t, err) 265 266 const stackCorruption = "value_stack_corruption" 267 const callStackCorruption = "call_stack_corruption" 268 const expectedReturnValue = 0x1 269 m := &wasm.Module{ 270 TypeSection: []*wasm.FunctionType{ 271 {Params: []wasm.ValueType{}, Results: []wasm.ValueType{wasm.ValueTypeI32}, ResultNumInUint64: 1}, 272 {Params: []wasm.ValueType{}, Results: []wasm.ValueType{}}, 273 }, 274 FunctionSection: []wasm.Index{ 275 wasm.Index(0), 276 wasm.Index(0), 277 wasm.Index(0), 278 }, 279 CodeSection: []*wasm.Code{ 280 { 281 // value_stack_corruption 282 Body: []byte{ 283 wasm.OpcodeCall, 0, // Call host function to shrink Goroutine stack 284 // We expect this value is returned, but if the stack is allocated on 285 // goroutine stack, we write this expected value into the old-location of 286 // stack. 287 wasm.OpcodeI32Const, expectedReturnValue, 288 wasm.OpcodeEnd, 289 }, 290 }, 291 { 292 // call_stack_corruption 293 Body: []byte{ 294 wasm.OpcodeCall, 3, // Call the wasm function below. 295 // At this point, call stack's memory looks like [call_stack_corruption, index3] 296 // With this function call it should end up [call_stack_corruption, host func] 297 // but if the call-frame stack is allocated on goroutine stack, we exit the native code 298 // with [call_stack_corruption, index3] (old call frame stack) with HostCall status code, 299 // and end up trying to call index3 as a host function which results in nil pointer exception. 300 wasm.OpcodeCall, 0, 301 wasm.OpcodeI32Const, expectedReturnValue, 302 wasm.OpcodeEnd, 303 }, 304 }, 305 {Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, 306 }, 307 ImportSection: []*wasm.Import{{Module: hostModuleName, Name: hostFnName, DescFunc: 1}}, 308 ExportSection: []*wasm.Export{ 309 {Type: wasm.ExternTypeFunc, Index: 1, Name: stackCorruption}, 310 {Type: wasm.ExternTypeFunc, Index: 2, Name: callStackCorruption}, 311 }, 312 ID: wasm.ModuleID{1}, 313 } 314 m.BuildFunctionDefinitions() 315 316 err = s.Engine.CompileModule(testCtx, m, nil) 317 require.NoError(t, err) 318 319 mi, err := s.Instantiate(testCtx, ns, m, t.Name(), nil) 320 require.NoError(t, err) 321 322 for _, fnName := range []string{stackCorruption, callStackCorruption} { 323 fnName := fnName 324 t.Run(fnName, func(t *testing.T) { 325 ret, err := mi.ExportedFunction(fnName).Call(testCtx) 326 require.NoError(t, err) 327 328 require.Equal(t, uint32(expectedReturnValue), uint32(ret[0])) 329 }) 330 } 331 } 332 333 func TestCallEngine_builtinFunctionTableGrow(t *testing.T) { 334 ce := &callEngine{ 335 stack: []uint64{ 336 0xff, // pseudo-ref 337 1, // num 338 // Table Index = 0 (lower 32-bits), but the higher bits (32-63) are all sets, 339 // which happens if the previous value on that stack location was 64-bit wide. 340 0xffffffff << 32, 341 }, 342 stackContext: stackContext{stackPointer: 3}, 343 } 344 345 table := &wasm.TableInstance{References: []wasm.Reference{}, Min: 10} 346 ce.builtinFunctionTableGrow(context.Background(), []*wasm.TableInstance{table}) 347 348 require.Equal(t, 1, len(table.References)) 349 require.Equal(t, uintptr(0xff), table.References[0]) 350 } 351 352 func ptrAsUint64(f *function) uint64 { 353 return uint64(uintptr(unsafe.Pointer(f))) 354 } 355 356 func TestCallEngine_deferredOnCall(t *testing.T) { 357 f1 := &function{source: &wasm.FunctionInstance{ 358 Definition: newMockFunctionDefinition("1"), 359 Type: &wasm.FunctionType{ParamNumInUint64: 2}, 360 }} 361 f2 := &function{source: &wasm.FunctionInstance{ 362 Definition: newMockFunctionDefinition("2"), 363 Type: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3}, 364 }} 365 f3 := &function{source: &wasm.FunctionInstance{ 366 Definition: newMockFunctionDefinition("3"), 367 Type: &wasm.FunctionType{ResultNumInUint64: 1}, 368 }} 369 370 ce := &callEngine{ 371 stack: []uint64{ 372 0xff, 0xff, // dummy argument for f1 373 0, 0, 0, 0, 374 0xcc, 0xcc, // local variable for f1. 375 // <----- stack base point of f2 (top) == index 8. 376 0xaa, 0xaa, 0xdeadbeaf, // dummy argument for f2 (0xaa, 0xaa) and the reserved slot for result 0xdeadbeaf) 377 0, 0, ptrAsUint64(f1), 0, // callFrame 378 0xcc, 0xcc, 0xcc, // local variable for f2. 379 // <----- stack base point of f3 (top) == index 18 380 0xdeadbeaf, // the reserved slot for result 0xdeadbeaf) from f3. 381 0, 8 << 3, ptrAsUint64(f2), 0, // callFrame 382 }, 383 stackContext: stackContext{ 384 stackBasePointerInBytes: 18 << 3, // currently executed function (f3)'s base pointer. 385 stackPointer: 0xff, // dummy supposed to be reset to zero. 386 }, 387 moduleContext: moduleContext{ 388 fn: f3, // currently executed function (f3)! 389 moduleInstanceAddress: 0xdeafbeaf, 390 }, 391 } 392 393 beforeRecoverStack := ce.stack 394 395 err := ce.deferredOnCall(errors.New("some error")) 396 require.EqualError(t, err, `some error (recovered by wazero) 397 wasm stack trace: 398 3() 399 2() 400 1()`) 401 402 // After recover, the state of callEngine must be reset except that the underlying slices must be intact 403 // for the subsequent calls to avoid additional allocations on each call. 404 require.Equal(t, uint64(0), ce.stackBasePointerInBytes) 405 require.Equal(t, uint64(0), ce.stackPointer) 406 require.Equal(t, uintptr(0), ce.moduleInstanceAddress) 407 require.Equal(t, beforeRecoverStack, ce.stack) 408 409 // Keep f1, f2, and f3 alive until we reach here, as we access these functions from the uint64 raw pointers in the stack. 410 // In practice, they are guaranteed to be alive as they are held by moduleContext. 411 runtime.KeepAlive(f1) 412 runtime.KeepAlive(f2) 413 runtime.KeepAlive(f3) 414 } 415 416 func newMockFunctionDefinition(name string) api.FunctionDefinition { 417 return &mockFunctionDefinition{debugName: name, FunctionDefinition: &wasm.FunctionDefinition{}} 418 } 419 420 type mockFunctionDefinition struct { 421 debugName string 422 *wasm.FunctionDefinition 423 } 424 425 // DebugName implements the same method as documented on api.FunctionDefinition. 426 func (f *mockFunctionDefinition) DebugName() string { 427 return f.debugName 428 } 429 430 // ParamTypes implements api.FunctionDefinition ParamTypes. 431 func (f *mockFunctionDefinition) ParamTypes() []wasm.ValueType { 432 return []wasm.ValueType{} 433 } 434 435 // ResultTypes implements api.FunctionDefinition ResultTypes. 436 func (f *mockFunctionDefinition) ResultTypes() []wasm.ValueType { 437 return []wasm.ValueType{} 438 } 439 440 func TestCallEngine_initializeStack(t *testing.T) { 441 const i32 = wasm.ValueTypeI32 442 const stackSize = 10 443 const initialVal = ^uint64(0) 444 tests := []struct { 445 name string 446 funcType *wasm.FunctionType 447 args []uint64 448 expStackPointer uint64 449 expStack [stackSize]uint64 450 }{ 451 { 452 name: "no param/result", 453 funcType: &wasm.FunctionType{}, 454 expStackPointer: callFrameDataSizeInUint64, 455 expStack: [stackSize]uint64{ 456 0, 0, 0, // zeroed call frame 457 initialVal, initialVal, initialVal, initialVal, initialVal, initialVal, initialVal, 458 }, 459 }, 460 { 461 name: "no result", 462 funcType: &wasm.FunctionType{ 463 Params: []wasm.ValueType{i32, i32}, 464 ParamNumInUint64: 2, 465 }, 466 args: []uint64{0xdeadbeaf, 0xdeadbeaf}, 467 expStackPointer: callFrameDataSizeInUint64 + 2, 468 expStack: [stackSize]uint64{ 469 0xdeadbeaf, 0xdeadbeaf, // arguments 470 0, 0, 0, // zeroed call frame 471 initialVal, initialVal, initialVal, initialVal, initialVal, 472 }, 473 }, 474 { 475 name: "no param", 476 funcType: &wasm.FunctionType{ 477 Results: []wasm.ValueType{i32, i32, i32}, 478 ResultNumInUint64: 3, 479 }, 480 expStackPointer: callFrameDataSizeInUint64 + 3, 481 expStack: [stackSize]uint64{ 482 initialVal, initialVal, initialVal, // reserved slots for results 483 0, 0, 0, // zeroed call frame 484 initialVal, initialVal, initialVal, initialVal, 485 }, 486 }, 487 { 488 name: "params > results", 489 funcType: &wasm.FunctionType{ 490 Params: []wasm.ValueType{i32, i32, i32, i32, i32}, 491 ParamNumInUint64: 5, 492 Results: []wasm.ValueType{i32, i32, i32}, 493 ResultNumInUint64: 3, 494 }, 495 args: []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf}, 496 expStackPointer: callFrameDataSizeInUint64 + 5, 497 expStack: [stackSize]uint64{ 498 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 499 0, 0, 0, // zeroed call frame 500 initialVal, initialVal, 501 }, 502 }, 503 { 504 name: "params == results", 505 funcType: &wasm.FunctionType{ 506 Params: []wasm.ValueType{i32, i32, i32, i32, i32}, 507 ParamNumInUint64: 5, 508 Results: []wasm.ValueType{i32, i32, i32, i32, i32}, 509 ResultNumInUint64: 5, 510 }, 511 args: []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf}, 512 expStackPointer: callFrameDataSizeInUint64 + 5, 513 expStack: [stackSize]uint64{ 514 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 515 0, 0, 0, // zeroed call frame 516 initialVal, initialVal, 517 }, 518 }, 519 { 520 name: "params < results", 521 funcType: &wasm.FunctionType{ 522 Params: []wasm.ValueType{i32, i32, i32}, 523 ParamNumInUint64: 3, 524 Results: []wasm.ValueType{i32, i32, i32, i32, i32}, 525 ResultNumInUint64: 5, 526 }, 527 args: []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf}, 528 expStackPointer: callFrameDataSizeInUint64 + 5, 529 expStack: [stackSize]uint64{ 530 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 531 initialVal, initialVal, // reserved for results 532 0, 0, 0, // zeroed call frame 533 initialVal, initialVal, 534 }, 535 }, 536 } 537 538 for _, tc := range tests { 539 tc := tc 540 t.Run(tc.name, func(t *testing.T) { 541 initialStack := make([]uint64, stackSize) 542 for i := range initialStack { 543 initialStack[i] = initialVal 544 } 545 ce := &callEngine{stack: initialStack} 546 ce.initializeStack(tc.funcType, tc.args) 547 require.Equal(t, tc.expStackPointer, ce.stackPointer) 548 require.Equal(t, tc.expStack[:], ce.stack) 549 }) 550 } 551 } 552 553 func Test_callFrameOffset(t *testing.T) { 554 require.Equal(t, 1, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 0, ResultNumInUint64: 1})) 555 require.Equal(t, 10, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 5, ResultNumInUint64: 10})) 556 require.Equal(t, 100, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 50, ResultNumInUint64: 100})) 557 require.Equal(t, 1, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 1, ResultNumInUint64: 0})) 558 require.Equal(t, 10, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 10, ResultNumInUint64: 5})) 559 require.Equal(t, 100, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 100, ResultNumInUint64: 50})) 560 } 561 562 func TestCallEngine_builtinFunctionFunctionListenerBefore(t *testing.T) { 563 nextContext, currentContext, prevContext := context.Background(), context.Background(), context.Background() 564 565 f := &function{ 566 source: &wasm.FunctionInstance{ 567 Definition: newMockFunctionDefinition("1"), 568 Type: &wasm.FunctionType{ParamNumInUint64: 3}, 569 }, 570 parent: &code{ 571 listener: mockListener{ 572 before: func(ctx context.Context, def api.FunctionDefinition, paramValues []uint64) context.Context { 573 require.Equal(t, currentContext, ctx) 574 require.Equal(t, []uint64{2, 3, 4}, paramValues) 575 return nextContext 576 }, 577 }, 578 }, 579 } 580 ce := &callEngine{ 581 ctx: currentContext, stack: []uint64{0, 1, 2, 3, 4, 5}, 582 stackContext: stackContext{stackBasePointerInBytes: 16}, 583 contextStack: &contextStack{self: prevContext}, 584 } 585 ce.builtinFunctionFunctionListenerBefore(ce.ctx, f) 586 587 // Contexts must be stacked. 588 require.Equal(t, currentContext, ce.contextStack.self) 589 require.Equal(t, prevContext, ce.contextStack.prev.self) 590 } 591 592 func TestCallEngine_builtinFunctionFunctionListenerAfter(t *testing.T) { 593 currentContext, prevContext := context.Background(), context.Background() 594 f := &function{ 595 source: &wasm.FunctionInstance{ 596 Definition: newMockFunctionDefinition("1"), 597 Type: &wasm.FunctionType{ResultNumInUint64: 1}, 598 }, 599 parent: &code{ 600 listener: mockListener{ 601 after: func(ctx context.Context, def api.FunctionDefinition, err error, resultValues []uint64) { 602 require.Equal(t, currentContext, ctx) 603 require.Equal(t, []uint64{5}, resultValues) 604 }, 605 }, 606 }, 607 } 608 609 ce := &callEngine{ 610 ctx: currentContext, stack: []uint64{0, 1, 2, 3, 4, 5}, 611 stackContext: stackContext{stackBasePointerInBytes: 40}, 612 contextStack: &contextStack{self: prevContext}, 613 } 614 ce.builtinFunctionFunctionListenerAfter(ce.ctx, f) 615 616 // Contexts must be popped. 617 require.Nil(t, ce.contextStack) 618 require.Equal(t, prevContext, ce.ctx) 619 } 620 621 type mockListener struct { 622 before func(ctx context.Context, def api.FunctionDefinition, paramValues []uint64) context.Context 623 after func(ctx context.Context, def api.FunctionDefinition, err error, resultValues []uint64) 624 } 625 626 func (m mockListener) Before(ctx context.Context, def api.FunctionDefinition, paramValues []uint64) context.Context { 627 return m.before(ctx, def, paramValues) 628 } 629 630 func (m mockListener) After(ctx context.Context, def api.FunctionDefinition, err error, resultValues []uint64) { 631 m.after(ctx, def, err, resultValues) 632 }