Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support mocking time.Sleep, add trap.Direct #28

Merged
merged 1 commit into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ jobs:

- name: Check spelling of files
uses: crate-ci/typos@master
continue-on-error: true
continue-on-error: false
with:
files: ./
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ func main(){
}
```

Trap also have a helper function called `Direct(fn)`, which can be used to bypass any trap and mock interceptors, calling directly into the original function.

## Mock
Mock simplifies the process of setting up Trap interceptors.

Expand Down Expand Up @@ -271,7 +273,7 @@ mock.Mock(v.Method, interceptor)
mock.Mock(closure, interceptor)
```

Arguments:
Parameters:
- If `fn` is a simple function(i.e. a package level function, or a function owned by a type, or a closure(yes, we do support mocking closures)),then all call to that function will be intercepted,
- If `fn` is a method(i.e. `file.Read`),then only call to the instance will be intercepted, other instances will not be affected

Expand Down Expand Up @@ -323,6 +325,47 @@ func TestMethodMock(t *testing.T){

**Notice for mocking stdlib**: due to performance and security impact, only a few packages and functions of stdlib can be mocked, the list can be found at [runtime/mock/stdlib.md](./runtime/mock/stdlib.md). If you want to mock additional stdlib functions, please discussion in [Issue#6](https://github.com/xhd2015/xgo/issues/6).

## Patch
The `runtime/mock` package also provides another api:
- `Patch(fn,replacer) func()`

Parameters:
- `fn` same as described in [Mock](#mock) section
- `replacer` another function that will replace `fn`

NOTE: `fn` and `replacer` should have the same signature.

Return:
- a `func()` can be used to remove the replacer earlier before current goroutine exits

Patch replaces the given `fn` with `replacer` in current goroutine. It will remove the replacer once current goroutine exits.

Example:
```go
package patch_test

import (
"testing"

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

func greet(s string) string {
return "hello " + s
}

func TestPatchFunc(t *testing.T) {
mock.Patch(greet, func(s string) string {
return "mock " + s
})

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

## Trace
It is painful when debugging with a deep call stack.

Expand Down
42 changes: 42 additions & 0 deletions README_zh_cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ func main(){
}
```

Trap还提供了一个`Direct(fn)`的函数, 用于跳过拦截器, 直接调用到原始的函数。

## Mock
Mock简化了设置拦截器的步骤, 并允许仅对特定的函数进行拦截。

Expand Down Expand Up @@ -317,6 +319,46 @@ func TestMethodMock(t *testing.T){

**关于标准库Mock的注意事项**: 出于性能和安全考虑, 标准库中只有一部分包和函数能被Mock, 这个List可以在[runtime/mock/stdlib.md](./runtime/mock/stdlib.md)找到. 如果你需要Mock的标准库函数不在列表中, 可以在[Issue#6](https://github.com/xhd2015/xgo/issues/6)中进行评论。

## Patch
`runtime/mock`还提供了另一个API:
- `Patch(fn,replacer) func()`

参数:
- `fn` 与[Mock](#mock)中的第一个参数相同
- `replacer`一个用来替换`fn`的函数

注意: `replacer`应当和`fn`具有同样的签名。

返回值:
- 一个`func()`, 用来提前移除`replacer`

Patch将`fn`替换为`replacer`,这个替换仅对当前goroutine生效.在当前Goroutine退出后, `replacer`被自动移除。

例子:
```go
package patch_test

import (
"testing"

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

func greet(s string) string {
return "hello " + s
}

func TestPatchFunc(t *testing.T) {
mock.Patch(greet, func(s string) string {
return "mock " + s
})

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

## Trace
在调试一个非常深的调用栈时, 通常会感觉非常痛苦, 并且效率低下。
Expand Down
48 changes: 46 additions & 2 deletions cmd/xgo/patch_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/xhd2015/xgo/cmd/xgo/patch"
"github.com/xhd2015/xgo/support/filecopy"
Expand All @@ -20,6 +21,10 @@ func patchRuntimeAndTesting(goroot string) error {
if err != nil {
return err
}
err = patchRuntimeTime(goroot)
if err != nil {
return err
}
return nil
}

Expand Down Expand Up @@ -80,15 +85,15 @@ func addRuntimeFunctions(goroot string, goVersion *goinfo.GoVersion, xgoSrc stri
}

func patchRuntimeProc(goroot string) error {
procFile := filepath.Join(goroot, "src", "runtime", "proc.go")
anchors := []string{
"func main() {",
"doInit(", "runtime_inittask", ")", // first doInit for runtime
"doInit(", // second init for main
"close(main_init_done)",
"\n",
}
procGo := filepath.Join(goroot, "src", "runtime", "proc.go")
err := editFile(procGo, func(content string) (string, error) {
err := editFile(procFile, func(content string) (string, error) {
content = addContentAfter(content, "/*<begin set_init_finished_mark>*/", "/*<end set_init_finished_mark>*/", anchors, patch.RuntimeProcPatch)

// goexit1() is called for every exited goroutine
Expand Down Expand Up @@ -132,3 +137,42 @@ func patchRuntimeTesting(goroot string) error {
return content, nil
})
}

// only required if need to mock time.Sleep
func patchRuntimeTime(goroot string) error {
runtimeTimeFile := filepath.Join(goroot, "src", "runtime", "time.go")
timeSleepFile := filepath.Join(goroot, "src", "time", "sleep.go")

err := editFile(runtimeTimeFile, func(content string) (string, error) {
content = replaceContentAfter(content,
"/*<begin redirect_runtime_sleep>*/", "/*<end redirect_runtime_sleep>*/",
[]string{},
"//go:linkname timeSleep time.Sleep\nfunc timeSleep(ns int64) {",
"//go:linkname timeSleep time.runtimeSleep\nfunc timeSleep(ns int64) {",
)
return content, nil
})
if err != nil {
return err
}

err = editFile(timeSleepFile, func(content string) (string, error) {
content = replaceContentAfter(content,
"/*<begin replace_sleep_with_runtimesleep>*/", "/*<end replace_sleep_with_runtimesleep>*/",
[]string{},
"func Sleep(d Duration)",
strings.Join([]string{
"func runtimeSleep(d Duration)",
"func Sleep(d Duration){",
" runtimeSleep(d)",
"}",
}, "\n"),
)
return content, nil
})
if err != nil {
return err
}

return nil
}
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.15"
const REVISION = "05e21215595deccfc08d49cb2d502e0d48b3cf4b+1"
const NUMBER = 151
const REVISION = "2861a46387df90bcadae7651dc6e0d2db8ab0148+1"
const NUMBER = 152

func getRevision() string {
return fmt.Sprintf("%s %s BUILD_%d", VERSION, REVISION, NUMBER)
Expand Down
14 changes: 12 additions & 2 deletions patch/ctxt/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func SkipPackageTrap() bool {

// allow http
pkgPath := GetPkgPath()
if pkgPath == "net/http" || pkgPath == "net" || pkgPath == "time" || pkgPath == "os" || pkgPath == "os/exec" {
if _, ok := stdWhitelist[pkgPath]; ok {
return false
}
return true
Expand All @@ -45,11 +45,21 @@ func SkipPackageTrap() bool {
}

var stdWhitelist = map[string]map[string]bool{
// "runtime": map[string]bool{
// "timeSleep": true,
// },
"os": map[string]bool{
// starts with Get
"OpenFile": true,
},
"time": map[string]bool{
"Now": true,
"Now": true,
// time.Sleep is special:
// if trapped like normal functions
// runtime/time.go:178:6: ns escapes to heap, not allowed in runtime
// there are special handling of this, see cmd/xgo/patch_runtime patchRuntimeTime
"Sleep": true, // NOTE: time.Sleep links to runtime.timeSleep
"NewTicker": true,
"Time.Format": true,
},
"os/exec": map[string]bool{
Expand Down
5 changes: 4 additions & 1 deletion patch/syntax/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@ func rewriteStdAndGenericFuncs(funcDecls []*DeclInfo, pkgPath string) {
if fn.Closure {
continue
}
if fn.FuncDecl.Body == nil {
// no body, may be linked
continue
}

// stdlib and generic
if !base.Flag.Std {
if !fn.Generic {
continue
}
}

fnDecl := fn.FuncDecl
pos := fn.FuncDecl.Pos()

Expand Down
2 changes: 1 addition & 1 deletion patch/syntax/syntax.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ func shouldTrap() bool {
}

pkgPath := xgo_ctxt.GetPkgPath()
if pkgPath == "" || pkgPath == "runtime" || strings.HasPrefix(pkgPath, "runtime/") || strings.HasPrefix(pkgPath, "internal/") || isSkippableSpecialPkg() {
if pkgPath == "" || strings.HasPrefix(pkgPath, "runtime/") || strings.HasPrefix(pkgPath, "internal/") || isSkippableSpecialPkg() {
// runtime/internal should not be rewritten
// internal/api has problem with the function register
return false
Expand Down
11 changes: 5 additions & 6 deletions patch/trap.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@ func CanInsertTrapOrLink(fn *ir.Func) (string, bool) {
return linkName, false
// ir.Dump("after:", fn)
}
// disable all stdlib IR rewrite
if base.Flag.Std {
// NOTE: stdlib are rewritten by source
return "", false
}
if xgo_ctxt.SkipPackageTrap() {
return "", false
}
Expand Down Expand Up @@ -339,12 +344,6 @@ func CanInsertTrapOrLink(fn *ir.Func) (string, bool) {
return "", false
}

// disable part of stdlibs
if base.Flag.Std {
// NOTE: stdlib are rewritten by source
return "", false
}

// func marked nosplit will skip trap because
// inserting traps when -gcflags=-N -l enabled
// would cause stack overflow 792 bytes
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.15"
const REVISION = "05e21215595deccfc08d49cb2d502e0d48b3cf4b+1"
const NUMBER = 151
const REVISION = "2861a46387df90bcadae7651dc6e0d2db8ab0148+1"
const NUMBER = 152

// these fields will be filled by compiler
const XGO_VERSION = ""
Expand Down
46 changes: 42 additions & 4 deletions runtime/mock/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
# API Summary
Mock exposes 3 primary APIs to users:
Mock exposes 3 `Mock` APIs to users:
- `Mock(fn, interceptor)` - for **99%** scenarios

- `MockByName(pkgPath, name, interceptor)` - for **unexported** function

- `MockMethodByName(instance, name, interceptor)` - for **unexported** method

Under 99% circumstances, developer should use `Mock` as long as possible because it does not involve hard coded name or package path.
And another 3 `Patch` APIs:
- `Patch(fn, replacer)` - for **99%** scenarios

- `PatchByName(pkgPath, name, replacer)` - for **unexported** function

- `PatchMethodByName(instance, name, replacer)` - for **unexported** method

Under 99% circumstances, developer should use `Mock` or `Patch` as long as possible because it does not involve hard coded name or package path.

The later two, `MockByName` and `MockMethodByName` are used where the target method cannot be accessed due to unexported, so they must be referenced by hard coded strings.

All 3 mock APIs take in an `InterceptorFunc` as the last argument, and returns a `func()`. The returned `func()` can be used to clear the passed in interceptor.

# Difference between `Mock` and `Patch`
The difference lies at their last parameter:
- `Mock` accepts an `InterceptorFunc`
- `Patch` accepts a function with the same signature as the first parameter

# Scope
Based on the timing when `Mock`,`MockByName` or `MockMethodByName` is called, the interceptor has different behaviors:
Based on the timing when `Mock*`,or `Patch*` is called, the interceptor has different behaviors:
- If called from `init`, then all goroutines will be mocked,
- Otherwise, `Mock` is called after `init`, then the mock interceptor will only be effective for current gorotuine, other goroutines are not affected.
- Otherwise, `Mock*` or `Patch*` is called after `init`, then the mock interceptor will only be effective for current gorotuine, other goroutines are not affected.

# Interceptor
Signature: `type InterceptorFunc func(ctx context.Context, fn *core.FuncInfo, args core.Object, results core.Object) error`
Expand Down Expand Up @@ -207,4 +219,30 @@ func TestMockGenericFunc(t *testing.T) {
t.Fatalf("expect ToString[string](%q) not affected, actual: %q", expectStr, outputStr)
}
}
```

# Patch
```go
package patch_test

import (
"testing"

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

func greet(s string) string {
return "hello " + s
}

func TestPatchFunc(t *testing.T) {
mock.Patch(greet, func(s string) string {
return "mock " + s
})

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