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