Skip to content

Commit

Permalink
update README
Browse files Browse the repository at this point in the history
  • Loading branch information
xhd2015 committed May 19, 2024
1 parent efe86af commit b01ce3a
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 272 deletions.
292 changes: 150 additions & 142 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

[![Go Reference](https://pkg.go.dev/badge/github.com/xhd2015/xgo.svg)](https://pkg.go.dev/github.com/xhd2015/xgo)
[![Go Report Card](https://goreportcard.com/badge/github.com/xhd2015/xgo)](https://goreportcard.com/report/github.com/xhd2015/xgo)
[![Slack Widget](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=red)](https://join.slack.com/t/golang-xgo/shared_invite/zt-2ixe161jb-3XYTDn37U1ZHZJSgnQi6sg)
[![Go Coverage](https://img.shields.io/badge/Coverage-82.6%25-brightgreen)](https://github.com/xhd2015/xgo/actions)
[![CI](https://github.com/xhd2015/xgo/workflows/Go/badge.svg)](https://github.com/xhd2015/xgo/actions)
[![Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
Expand All @@ -11,9 +12,9 @@

`xgo` provides *all-in-one* test utilities for golang, including:

- [Trap](#trap)
- [Mock](#mock)
- [Mock](#patch)
- [Trace](#trace)
- [Trap](#trap)
- [Incremental Coverage](#incremental-coverage)

As for the monkey patching part, `xgo` works as a preprocessor for `go run`,`go build`, and `go test`(see our [blog](https://blog.xhd2015.xyz/posts/xgo-monkey-patching-in-go-using-toolexec)).
Expand Down Expand Up @@ -133,134 +134,99 @@ FAIL
The above demo can be found at [doc/demo](./doc/demo).

# API
## Trap
It **preprocess** the source code and IR(Intermediate Representation) before invoking `go`, adding missing abilities to go program by cooperating with(or hacking) the go compiler.

<a id="patch"></a>

Trap allows developer to intercept function execution on the fly.
## mock.Patch
Patch given function within current goroutine.

Trap is the core of `xgo` as it is the basis of other abilities like Mock and Trace.

The following example logs function execution trace by adding a Trap interceptor:
The API:
- `Patch(fn,replacer) func()`

(check [test/testdata/trap/trap.go](test/testdata/trap/trap.go) for more details.)
Cheatsheet:
```go
package main
// package level func
mock.Patch(SomeFunc, mockFunc)

import (
"context"
"fmt"
// per-instance method
// only the bound instance `v` will be mocked
// `v` can be either a struct or an interface
mock.Patch(v.Method, mockMethod)

"github.com/xhd2015/xgo/runtime/core"
"github.com/xhd2015/xgo/runtime/trap"
)
// per-TParam generic function
// only the specified `int` version will be mocked
mock.Patch(GenericFunc[int], mockFuncInt)

func init() {
trap.AddInterceptor(&trap.Interceptor{
Pre: func(ctx context.Context, f *core.FuncInfo, args core.Object, results core.Object) (interface{}, error) {
if f.Name == "A" {
fmt.Printf("trap A\n")
return nil, nil
}
if f.Name == "B" {
fmt.Printf("abort B\n")
return nil, trap.ErrAbort
}
return nil, nil
},
})
}
// per TParam and instance generic method
v := GenericStruct[int]
mock.Mock(v.Method, mockMethod)

func main() {
A()
B()
}
// closure can also be mocked
// less used, but also supported
mock.Mock(closure, mockFunc)
```

func A() {
fmt.Printf("A\n")
}
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,
- `replacer` another function that will replace `fn`

func B() {
fmt.Printf("B\n")
}
```
Scope:
- If `Patch` is called from `init`, then all goroutines will be mocked.
- Otherwise, `Patch` is called after `init`, then the mock interceptor will only be effective for current goroutine, other goroutines are not affected.

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

```sh
go run ./
# output:
# A
# B
```
Return:
- a `func()` can be used to remove the replacer earlier before current goroutine exits

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

```sh
xgo run ./
# output:
# trap A
# A
# abort B
```
Example:
```go
package patch_test

`AddInterceptor()` add given interceptor to either global or local, depending on whether it is called from `init` or after `init`:
- Before `init`: effective globally for all goroutines,
- After `init`: effective only for current goroutine, and will be cleared after current goroutine exits.
import (
"testing"

When `AddInterceptor()` is called after `init`, it will return a dispose function to clear the interceptor earlier before current goroutine exits.
"github.com/xhd2015/xgo/runtime/mock"
)

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

```go
func main(){
clear := trap.AddInterceptor(...)
defer clear()
...
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)
}
}
```

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.
NOTE: `Patch` and `Mock`(below) supports top-level variables and consts, see [runtime/mock/MOCK_VAR_CONST.md](runtime/mock/MOCK_VAR_CONST.md).

**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 file a discussion in [Issue#6](https://github.com/xhd2015/xgo/issues/6).

## Mock
Mock simplifies the process of setting up Trap interceptors.
`runtime/mock` also provides another API called `Mock`, which is similar to `Patch`.

The the only difference between them lies in the the second parameter: `Mock` accepts an interceptor.

`Mock` can be used where `Patch` cannot be used, such as functions with unexported type.

> API details: [runtime/mock/README.md](runtime/mock)
The Mock API:
- `Mock(fn, interceptor)`

Cheatsheet:
```go
// package level func
mock.Mock(SomeFunc, interceptor)

// per-instance method
// only the bound instance `v` will be mocked
// `v` can be either a struct or an interface
mock.Mock(v.Method, interceptor)

// per-TParam generic function
// only the specified `int` version will be mocked
mock.Mock(GenericFunc[int], interceptor)

// per TParam and instance generic method
v := GenericStruct[int]
mock.Mock(v.Method, interceptor)

// closure can also be mocked
// less used, but also supported
mock.Mock(closure, interceptor)
```

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,
- `fn` same as described in [Patch](#patch) section
- If `fn` is a method(i.e. `file.Read`),then only call to the instance will be intercepted, other instances will not be affected

Scope:
- If `Mock` is 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 goroutine, other goroutines are not affected.

Interceptor Signature: `func(ctx context.Context, fn *core.FuncInfo, args core.Object, results core.Object) error`
- If the interceptor returns `nil`, then the target function is mocked,
- If the interceptor returns `mock.ErrCallOld`, then the target function is called again,
Expand Down Expand Up @@ -303,51 +269,6 @@ 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 file a 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)
}
}
```

NOTE: `Mock` and `Patch` supports top-level variables and consts, see [runtime/mock/MOCK_VAR_CONST.md](runtime/mock/MOCK_VAR_CONST.md).

## Trace
> Trace might be the most powerful tool provided by xgo, this blog has a more thorough example: https://blog.xhd2015.xyz/posts/xgo-trace_a-powerful-visualization-tool-in-go
Expand Down Expand Up @@ -417,6 +338,93 @@ func TestTrace(t *testing.T) {
}
```
The trace will only include `B()` and `C()`.
## Trap
Xgo **preprocess** the source code and IR(Intermediate Representation) before invoking `go`, providing a chance for user to intercept any function when called.

Trap allows developer to intercept function execution on the fly.

Trap is the core of `xgo` as it is the basis of other abilities like Mock and Trace.

The following example logs function execution trace by adding a Trap interceptor:

(check [test/testdata/trap/trap.go](test/testdata/trap/trap.go) for more details.)
```go
package main

import (
"context"
"fmt"

"github.com/xhd2015/xgo/runtime/core"
"github.com/xhd2015/xgo/runtime/trap"
)

func init() {
trap.AddInterceptor(&trap.Interceptor{
Pre: func(ctx context.Context, f *core.FuncInfo, args core.Object, results core.Object) (interface{}, error) {
if f.Name == "A" {
fmt.Printf("trap A\n")
return nil, nil
}
if f.Name == "B" {
fmt.Printf("abort B\n")
return nil, trap.ErrAbort
}
return nil, nil
},
})
}

func main() {
A()
B()
}

func A() {
fmt.Printf("A\n")
}

func B() {
fmt.Printf("B\n")
}
```

Run with `go`:

```sh
go run ./
# output:
# A
# B
```

Run with `xgo`:

```sh
xgo run ./
# output:
# trap A
# A
# abort B
```

`AddInterceptor()` add given interceptor to either global or local, depending on whether it is called from `init` or after `init`:
- Before `init`: effective globally for all goroutines,
- After `init`: effective only for current goroutine, and will be cleared after current goroutine exits.

When `AddInterceptor()` is called after `init`, it will return a dispose function to clear the interceptor earlier before current goroutine exits.

Example:

```go
func main(){
clear := trap.AddInterceptor(...)
defer clear()
...
}
```

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.

## Incremental Coverage
The `xgo tool coverage` sub command extends go's builtin `go tool cover` for better visualization.
Expand Down
Loading

0 comments on commit b01ce3a

Please sign in to comment.