Skip to content

Commit

Permalink
fix patching type method
Browse files Browse the repository at this point in the history
  • Loading branch information
xhd2015 committed Apr 2, 2024
1 parent 6150ddb commit 3f5b60b
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 35 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ jobs:
run: ~/.xgo/bin/xgo revision

- name: Check Go Version
run: ~/.xgo/bin/xgo exec go version
run: ~/.xgo/bin/xgo exec go version

- name: Check spelling of files
uses: crate-ci/typos@master
continue-on-error: true
with:
files: ./
4 changes: 2 additions & 2 deletions cmd/xgo/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package main
import "fmt"

const VERSION = "1.0.12"
const REVISION = "f3d7271450fef6b7575368a82d6fe254c894a97e+1"
const NUMBER = 147
const REVISION = "9a1416bbf043a8466aa035b835f474e516adfe68+1"
const NUMBER = 149

func getRevision() string {
return fmt.Sprintf("%s %s BUILD_%d", VERSION, REVISION, NUMBER)
Expand Down
4 changes: 2 additions & 2 deletions runtime/core/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
)

const VERSION = "1.0.12"
const REVISION = "f3d7271450fef6b7575368a82d6fe254c894a97e+1"
const NUMBER = 147
const REVISION = "9a1416bbf043a8466aa035b835f474e516adfe68+1"
const NUMBER = 149

// these fields will be filled by compiler
const XGO_VERSION = ""
Expand Down
59 changes: 35 additions & 24 deletions runtime/mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,55 @@ import (
"github.com/xhd2015/xgo/runtime/trap"
)

// a marker to indicate the
// original function should be called
var ErrCallOld = errors.New("mock: call old")

type Interceptor func(ctx context.Context, fn *core.FuncInfo, args core.Object, results core.Object) error

// Mock setup mock on given function `fn`.
// `fn` can be a function or a method,
// when `fn` is a method, only the bound
// if `fn` is a method, only the bound
// instance will be mocked, other instances
// are not affected.
// The returned function can be used to cancel
// the passed interceptor.
func Mock(fn interface{}, interceptor Interceptor) func() {
return mockByFunc(fn, interceptor)
recvPtr, fnInfo, funcPC, trappingPC := getFunc(fn)
return mock(recvPtr, fnInfo, funcPC, trappingPC, interceptor)
}

func MockByName(pkgPath string, funcName string, interceptor Interceptor) func() {
funcInfo := functab.GetFuncByPkg(pkgPath, funcName)
if funcInfo == nil {
panic(fmt.Errorf("failed to setup mock for: %s.%s", pkgPath, funcName))
}
return mock(nil, funcInfo, 0, 0, interceptor)
recv, fn, funcPC, trappingPC := getFuncByName(pkgPath, funcName)
return mock(recv, fn, funcPC, trappingPC, interceptor)
}

// Can instance be nil?
func MockMethodByName(instance interface{}, method string, interceptor Interceptor) func() {
recvPtr, fn, funcPC, trappingPC := getMethodByName(instance, method)
return mock(recvPtr, fn, funcPC, trappingPC, interceptor)
}

func getFunc(fn interface{}) (recvPtr interface{}, fnInfo *core.FuncInfo, funcPC uintptr, trappingPC uintptr) {
// if the target function is a method, then a
// recv ptr must be given
recvPtr, fnInfo, funcPC, trappingPC = trap.InspectPC(fn)
if fnInfo == nil {
pc := reflect.ValueOf(fn).Pointer()
panic(fmt.Errorf("failed to setup mock for: %v", runtime.FuncForPC(pc).Name()))
}
return recvPtr, fnInfo, funcPC, trappingPC
}

func getFuncByName(pkgPath string, funcName string) (recvPtr interface{}, fn *core.FuncInfo, funcPC uintptr, trappingPC uintptr) {
fn = functab.GetFuncByPkg(pkgPath, funcName)
if fn == nil {
panic(fmt.Errorf("failed to setup mock for: %s.%s", pkgPath, funcName))
}
return nil, fn, 0, 0
}

func getMethodByName(instance interface{}, method string) (recvPtr interface{}, fn *core.FuncInfo, funcPC uintptr, trappingPC uintptr) {
// extract instance's reflect.Type
// use that type to query for reflect mapping in functab:
// reflectTypeMapping map[reflect.Type]map[string]*funcInfo
Expand All @@ -45,34 +69,25 @@ func MockMethodByName(instance interface{}, method string, interceptor Intercept
if funcMapping == nil {
panic(fmt.Errorf("failed to setup mock for type %T", instance))
}
fn := funcMapping[method]
fn = funcMapping[method]
if fn == nil {
panic(fmt.Errorf("failed to setup mock for: %T.%s", instance, method))
}

addr := reflect.New(t)
addr.Elem().Set(reflect.ValueOf(instance))
return mock(addr.Interface(), fn, 0, 0, interceptor)

return addr.Interface(), fn, 0, 0
}

// Deprecated: use Mock instead
func AddFuncInterceptor(fn interface{}, interceptor Interceptor) func() {
return mockByFunc(fn, interceptor)
return Mock(fn, interceptor)
}

// TODO: ensure them run in last?
// no abort, run mocks
// mocks are special in that they on run in pre stage
func mockByFunc(fn interface{}, interceptor Interceptor) func() {
// if the target function is a method, then a
// recv ptr must be given
recvPtr, fnInfo, funcPC, trappingPC := trap.InspectPC(fn)
if fnInfo == nil {
pc := reflect.ValueOf(fn).Pointer()
panic(fmt.Errorf("failed to setup mock for: %v", runtime.FuncForPC(pc).Name()))
}
return mock(recvPtr, fnInfo, funcPC, trappingPC, interceptor)
}

// if mockFnInfo is a function, mockRecvPtr is always nil
// if mockFnInfo is a method,
Expand Down Expand Up @@ -109,10 +124,6 @@ func mock(mockRecvPtr interface{}, mockFnInfo *core.FuncInfo, funcPC uintptr, tr
// check they pointing to the same variable
re := reflect.ValueOf(recvPtr).Elem().Interface()
me := reflect.ValueOf(mockRecvPtr).Elem().Interface()
if f.RecvPtr {
// compare pointer
// unsafe.Pointer(&re)
}
if re != me {
// if *recvPtr != *mockRecvPtr {
return nil, nil
Expand Down
14 changes: 10 additions & 4 deletions runtime/mock/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import (
)

func PatchByName(pkgPath string, funcName string, replacer interface{}) func() {
return MockByName(pkgPath, funcName, buildInterceptorFromPatch(replacer))
recvPtr, funcInfo, funcPC, trappingPC := getFuncByName(pkgPath, funcName)
return mock(recvPtr, funcInfo, funcPC, trappingPC, buildInterceptorFromPatch(recvPtr, replacer))
}

func PatchMethodByName(instance interface{}, method string, replacer interface{}) func() {
return MockMethodByName(instance, method, buildInterceptorFromPatch(replacer))
recvPtr, funcInfo, funcPC, trappingPC := getMethodByName(instance, method)
return mock(recvPtr, funcInfo, funcPC, trappingPC, buildInterceptorFromPatch(recvPtr, replacer))
}

func buildInterceptorFromPatch(replacer interface{}) func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
func buildInterceptorFromPatch(recvPtr interface{}, replacer interface{}) func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
v := reflect.ValueOf(replacer)
t := v.Type()
if t.Kind() != reflect.Func {
Expand All @@ -26,12 +28,16 @@ func buildInterceptorFromPatch(replacer interface{}) func(ctx context.Context, f
panic("replacer is nil")
}
nIn := t.NumIn()

// first arg ctx: true => [recv,args[1:]...]
// first arg ctx: false => [recv, args[0:]...]
return func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
// assemble arguments
callArgs := make([]reflect.Value, nIn)
src := 0
dst := 0
if fn.RecvType != "" {
if fn.RecvType != "" && recvPtr != nil {
// patching an instance method
src++
}
if fn.FirstArgCtx {
Expand Down
3 changes: 2 additions & 1 deletion runtime/mock/patch_go1.17.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
package mock

func Patch(fn interface{}, replacer interface{}) func() {
return Mock(fn, buildInterceptorFromPatch(replacer))
recvPtr, fnInfo, funcPC, trappingPC := getFunc(fn)
return mock(recvPtr, fnInfo, funcPC, trappingPC, buildInterceptorFromPatch(recvPtr, replacer))
}
3 changes: 2 additions & 1 deletion runtime/mock/patch_go1.18.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ package mock
// TODO: what if `fn` is a Type function
// instead of an instance method?
func Patch[T any](fn T, replacer T) func() {
return Mock(fn, buildInterceptorFromPatch(replacer))
recvPtr, fnInfo, funcPC, trappingPC := getFunc(fn)
return mock(recvPtr, fnInfo, funcPC, trappingPC, buildInterceptorFromPatch(recvPtr, replacer))
}
21 changes: 21 additions & 0 deletions runtime/test/patch/patch_type_method_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package patch

import (
"testing"

"github.com/xhd2015/xgo/runtime/mock"
)

func TestPatchTypeMethod(t *testing.T) {
ins := &struct_{
s: "world",
}
mock.Patch((*struct_).greet, func(ins *struct_) string {
return "mock " + ins.s
})

res := ins.greet()
if res != "mock world" {
t.Fatalf("expect patched result to be %q, actual: %q", "mock world", res)
}
}

0 comments on commit 3f5b60b

Please sign in to comment.