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

xgo test enables --trap-stdlib by default #166

Merged
merged 1 commit into from
May 30, 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
29 changes: 29 additions & 0 deletions .github/workflows/go-compatible.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go Compatible

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

jobs:

tests-with-older-go-versions:
strategy:
matrix:
os: [ ubuntu-latest]
go: [ '1.18' ,'1.20','1.21']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '${{ matrix.go }}'

- name: Test
run: go run ./script/run-test --reset-instrument --debug -v
2 changes: 1 addition & 1 deletion .github/workflows/go-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
os: [ windows-latest ]
go: [ '1.20' ]
go: [ '1.22' ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest]
go: [ '1.20' ]
go: [ '1.22' ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func TestPatchFunc(t *testing.T) {

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).
**Notice for mocking stdlib**: There are different modes for mocking stdlib functions,see [runtime/mock/stdlib.md](./runtime/mock/stdlib.md).

## Mock
`runtime/mock` also provides another API called `Mock`, which is similar to `Patch`.
Expand Down
2 changes: 1 addition & 1 deletion README_zh_cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func TestPatchFunc(t *testing.T) {

注意: `Mock`和`Patch`也支持对包级别的变量和常量进行mock, 见[runtime/mock/MOCK_VAR_CONST.md](runtime/mock/MOCK_VAR_CONST.md).

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

## Mock
`runtime/mock` 还提供了名为`Mock`的API, 它与`Patch`十分类似,唯一的区别是第二个参数接受一个拦截器。
Expand Down
14 changes: 12 additions & 2 deletions cmd/xgo/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ func parseOptions(cmd string, args []string) (*options, error) {
})
}

if cmd == "test" {
trapStdlib = true
}

for i := 0; i < nArg; i++ {
arg := args[i]
if !strings.HasPrefix(arg, "-") {
Expand Down Expand Up @@ -266,8 +270,14 @@ func parseOptions(cmd string, args []string) (*options, error) {
continue
}

if arg == "--trap-stdlib" {
trapStdlib = true
// supported flag: --trap-stdlib, --trap-stdlib=false, --trap-stdlib=true
trapStdlibFlag, trapStdlibVal := flag.TrySingleFlag([]string{"--trap-stdlib"}, arg)
if trapStdlibFlag != "" {
if trapStdlibVal == "" || trapStdlibVal == "true" {
trapStdlib = true
} else {
trapStdlib = false
}
continue
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/xgo/runtime_gen/core/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
)

const VERSION = "1.0.37"
const REVISION = "2e709f7b1620aea78fdc1dba282bcf6778dd6d51+1"
const NUMBER = 245
const REVISION = "1d56b338a2c930297d1285877665201ebc0e1077+1"
const NUMBER = 246

// these fields will be filled by compiler
const XGO_VERSION = ""
Expand Down
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.37"
const REVISION = "2e709f7b1620aea78fdc1dba282bcf6778dd6d51+1"
const NUMBER = 245
const REVISION = "1d56b338a2c930297d1285877665201ebc0e1077+1"
const NUMBER = 246

func getRevision() string {
revSuffix := ""
Expand Down
33 changes: 25 additions & 8 deletions patch/ctxt/stdlib.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package ctxt

import "strings"
import (
"cmd/compile/internal/types"
"strings"
)

var stdWhitelist = map[string]map[string]bool{
// "runtime": map[string]bool{
Expand Down Expand Up @@ -73,21 +76,35 @@ var stdBlocklist = map[string]map[string]bool{
"*": true,
},
// testing is not harmful
// "testing": map[string]bool{
// "*": true,
// },
// but may cause infinite loop?
// we may dig later or just add somce whitelist
"testing": map[string]bool{
"*": true,
},
"unsafe": map[string]bool{
"*": true,
},
}

func allowStdFunc(pkgPath string, funcName string) bool {
if XgoStdTrapDefaultAllow {
if stdBlocklist[pkgPath]["*"] || stdBlocklist[pkgPath][funcName] {
return false
}
if isWhitelistStdFunc(pkgPath, funcName) {
return true
}
if !XgoStdTrapDefaultAllow {
return false
}

if stdBlocklist[pkgPath]["*"] || stdBlocklist[pkgPath][funcName] {
return false
}
if !types.IsExported(funcName) {
// unexported func
return false
}
return true
}

func isWhitelistStdFunc(pkgPath string, funcName string) bool {
if stdWhitelist[pkgPath][funcName] {
return true
}
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.37"
const REVISION = "2e709f7b1620aea78fdc1dba282bcf6778dd6d51+1"
const NUMBER = 245
const REVISION = "1d56b338a2c930297d1285877665201ebc0e1077+1"
const NUMBER = 246

// these fields will be filled by compiler
const XGO_VERSION = ""
Expand Down
33 changes: 28 additions & 5 deletions runtime/mock/stdlib.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
# Limitation On Stdlib Functions
Stdlib functions like `os.ReadFile`, `io.Read` are widely used by go code. So installing a trap on these functions may have big impact on performance and security.
# Support Of Mocking Stdlib Functions
There are two modes for mocking stdlib functions,
1. Default-Allow Mode
2. Default-Disallow Mode

And as compiler treats stdlib from ordinary module differently, current implementation to support stdlib function is based on source code injection, which may causes build time to slow down.
These two modes can be switched by `--trap-stdlib` and `--trap-stdlib=false`.

So only a limited list of stdlib functions can be mocked. However, if there lacks some functions you may want to use, you can leave a comment in [Issue#6](https://github.com/xhd2015/xgo/issues/6) or fire an issue to let us know and add it.
When running `xgo test`, `--trap-stdlib` is assumed, you can turn it off with `--trap-stdlib=false`.

# Supported List
When running `xgo run` and `xgo build`, `--trap-stdlib=false` is assumed, you can turn it on with `--trap-stdlib`.

# `--trap-stdlib`: Default-Allow Mode
In this mode, most stdlib packages can be mocked, except `runtime`, `syscall`, `reflect`, `sync`, `sync/atomic`, `testing`, `unsafe`.

# `--trap-stdlib=false`: Default-Disallow Mode
In this mode, only a small list of stdlib functions can be mocked due to performance and security impact.

Rational: stdlib functions like `os.ReadFile`, `io.Read` are widely used by go code, installing a trap on these functions may have big impact on performance and security.

So in this mode only a limited list of stdlib functions can be mocked. However, if there lacks some functions you may want to use, you can leave a comment in [Issue#6](https://github.com/xhd2015/xgo/issues/6) or fire an issue to let us know and add it.

# Functions In Stdlib Calling `recover()` Cannot Be Mocked
When a function calls `recover()`, it will capture panic when used in defer.

However, since compiler treats stdlib from ordinary module differently, current implementation to support stdlib function is based on source code injection, which may causes build time to slow down, and also causes functions containing `recover()` to be invalid if rewritten, see https://github.com/xhd2015/xgo/issues/164.

This will be fixed in the long run, but before that, such functions in stdlib cannot be mocked.

NOTE: functions outside stdlib, even with calling `recover()`, are not affected since they are rewritten with IR, not source code.

# Supported List When `--trap-stdlib=false`
## `os`
- `Getenv`
- `Getwd`
Expand Down
12 changes: 12 additions & 0 deletions runtime/test/trap/trap_avoid_recursive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ func TestNakedTrapShouldAvoidRecursiveInterceptor(t *testing.T) {
var recurseBuf bytes.Buffer
trap.AddInterceptor(&trap.Interceptor{
Pre: func(ctx context.Context, f *core.FuncInfo, args, result core.Object) (data interface{}, err error) {
if f.Stdlib {
return nil, nil
}
fmt.Fprintf(&recurseBuf, "pre\n")
return nil, nil
},
Post: func(ctx context.Context, f *core.FuncInfo, args, result core.Object, data interface{}) (err error) {
if f.Stdlib {
return nil
}
fmt.Fprintf(&recurseBuf, "post\n")
return nil
},
Expand All @@ -41,10 +47,16 @@ func TestDeferredFuncShouldBeExecutedWhenAbort(t *testing.T) {
var recurseBuf bytes.Buffer
trap.AddInterceptor(&trap.Interceptor{
Pre: func(ctx context.Context, f *core.FuncInfo, args, result core.Object) (data interface{}, err error) {
if f.Stdlib {
return nil, nil
}
fmt.Fprintf(&recurseBuf, "pre\n")
return nil, trap.ErrAbort
},
Post: func(ctx context.Context, f *core.FuncInfo, args, result core.Object, data interface{}) (err error) {
if f.Stdlib {
return nil
}
fmt.Fprintf(&recurseBuf, "post\n")
return nil
},
Expand Down
15 changes: 15 additions & 0 deletions runtime/test/trap/trap_nested_interceptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ func TestNestedTrapShouldBeAllowedBySpecifyingMapping(t *testing.T) {
// list the names that can be nested
trap.AddInterceptor(&trap.Interceptor{
Pre: func(ctx context.Context, f *core.FuncInfo, args, result core.Object) (data interface{}, err error) {
if f.Stdlib {
return nil, nil
}
traceRecords.WriteString(fmt.Sprintf("call %s\n", f.IdentityName))
if f.IdentityName == "A0" {
// call A1 inside the interceptor
Expand All @@ -29,12 +32,18 @@ func TestNestedTrapShouldBeAllowedBySpecifyingMapping(t *testing.T) {
})
trap.AddFuncInterceptor(A1, &trap.Interceptor{
Pre: func(ctx context.Context, f *core.FuncInfo, args, result core.Object) (data interface{}, err error) {
if f.Stdlib {
return nil, nil
}
traceRecords.WriteString(fmt.Sprintf("call %s\n", f.IdentityName))
return
},
})
trap.AddFuncInterceptor(A2, &trap.Interceptor{
Pre: func(ctx context.Context, f *core.FuncInfo, args, result core.Object) (data interface{}, err error) {
if f.Stdlib {
return nil, nil
}
traceRecords.WriteString(fmt.Sprintf("call %s\n", f.IdentityName))
return
},
Expand Down Expand Up @@ -68,6 +77,9 @@ func TestNestedTrapPartialAllowShouldTakeEffect(t *testing.T) {
var traceRecords bytes.Buffer
trap.AddInterceptor(&trap.Interceptor{
Pre: func(ctx context.Context, f *core.FuncInfo, args, result core.Object) (data interface{}, err error) {
if f.Stdlib {
return nil, nil
}
traceRecords.WriteString(fmt.Sprintf("call %s\n", f.IdentityName))
if f.IdentityName == "B0" {
// call A1 inside the interceptor
Expand All @@ -80,6 +92,9 @@ func TestNestedTrapPartialAllowShouldTakeEffect(t *testing.T) {

trap.AddFuncInterceptor(B2, &trap.Interceptor{
Pre: func(ctx context.Context, f *core.FuncInfo, args, result core.Object) (data interface{}, err error) {
if f.Stdlib {
return nil, nil
}
traceRecords.WriteString(fmt.Sprintf("call %s\n", f.IdentityName))
return
},
Expand Down
6 changes: 6 additions & 0 deletions runtime/test/trap/trap_return_err_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ func TestTrapReturnedErrorShouldBeSet(t *testing.T) {
mockErr := errors.New("must not be an odd")
trap.AddInterceptor(&trap.Interceptor{
Pre: func(ctx context.Context, f *core.FuncInfo, args, result core.Object) (data interface{}, err error) {
if f.Stdlib {
return nil, nil
}
return nil, mockErr
},
})
Expand All @@ -30,6 +33,9 @@ func TestTrapPosReturnedErrorShouldBeSet(t *testing.T) {
mockErr := errors.New("must not be an odd")
trap.AddInterceptor(&trap.Interceptor{
Post: func(ctx context.Context, f *core.FuncInfo, args, result core.Object, data interface{}) error {
if f.Stdlib {
return nil
}
return mockErr
},
})
Expand Down
3 changes: 3 additions & 0 deletions runtime/test/trap_args/err_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ func TestSubErrShouldNotSetErrRes(t *testing.T) {
callAndCheck(func() {
err = subErr()
}, func(trapCtx context.Context, f *core.FuncInfo, args, result core.Object) error {
if f.Stdlib {
return nil
}
if f.LastResultErr {
t.Fatalf("expect f.LastResultErr to be false, actual: true")
}
Expand Down
6 changes: 6 additions & 0 deletions runtime/test/trap_args/trap_args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ func TestCtxArgWithRecvCanBeRecognized(t *testing.T) {
var callCount int
trap.AddInterceptor(&trap.Interceptor{
Pre: func(trapCtx context.Context, f *core.FuncInfo, args, result core.Object) (data interface{}, err error) {
if f.Stdlib {
return nil, nil
}
callCount++
if !f.FirstArgCtx {
t.Fatalf("expect first arg to be context")
Expand Down Expand Up @@ -80,6 +83,9 @@ func callAndCheck(fn func(), check func(trapCtx context.Context, f *core.FuncInf
var callCount int
trap.AddInterceptor(&trap.Interceptor{
Pre: func(trapCtx context.Context, f *core.FuncInfo, args, result core.Object) (data interface{}, err error) {
if f.Stdlib {
return nil, nil
}
if callCount == 0 {
callCount++
if pcName != f.FullName {
Expand Down
Loading
Loading