Skip to content

Commit

Permalink
make constant mock available for go1.20 and above
Browse files Browse the repository at this point in the history
  • Loading branch information
xhd2015 committed Apr 7, 2024
1 parent 5e64b66 commit fe2e2f3
Show file tree
Hide file tree
Showing 20 changed files with 220 additions and 63 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ func TestPatchFunc(t *testing.T) {
}
```

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

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

Expand Down Expand Up @@ -459,7 +461,9 @@ In conclusion, `xgo` and monkey are compared as the following:
|Per-Instance Method Mock|Y|N|
|Per-Goroutine Mock|Y|N|
|Per-Generic Type Mock|Y|Y|
|Closuer Mock|Y|Y|
|Var Mock|Y|N|
|Const Mock|Y|N|
|Closure Mock|Y|Y|
|Stack Trace|Y|N|
|General Trap|Y|N|
|Compatiblility|NO LIMIT|limited to amd64 and arm64|
Expand Down
4 changes: 4 additions & 0 deletions README_zh_cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ func TestPatchFunc(t *testing.T) {
}
```

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

## Trace
在调试一个非常深的调用栈时, 通常会感觉非常痛苦, 并且效率低下。

Expand Down Expand Up @@ -451,6 +453,8 @@ Xgo通过在IR(Intermediate Representation)重写来成功地避开了这些问
|实例级别方法Mock|Y|N|
|Goroutine级别Mock|Y|N|
|范型特定实例Mock|Y|Y|
|变量Mock|Y|N|
|常量Mock|Y|N|
|闭包/匿名函数Mock|Y|Y|
|堆栈收集|Y|N|
|通用拦截器|Y|N|
Expand Down
4 changes: 2 additions & 2 deletions cmd/xgo/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ func addContentAt(content string, beginMark string, endMark string, seq []string
}

func addContentAtIndex(content string, beginMark string, endMark string, seq []string, i int, before bool, addContent string) string {
offset, endOffset := strutil.SeqenceOffset(content, seq, i, before)
offset, endOffset := strutil.SequenceOffset(content, seq, i, before)
if offset < 0 {
panic(fmt.Errorf("sequence missing: %v", seq))
}
anotherOff, _ := strutil.SeqenceOffset(content[endOffset:], seq, i, false)
anotherOff, _ := strutil.SequenceOffset(content[endOffset:], seq, i, false)
if anotherOff >= 0 {
panic(fmt.Errorf("sequence duplicate: %v", seq))
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/xgo/patch_compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func patchCompilerInternal(goroot string, goVersion *goinfo.GoVersion) error {
if err != nil {
return fmt.Errorf("patching gc main:%w", err)
}
err = patchCompilerAstTypeCheck(goroot)
err = patchCompilerAstTypeCheck(goroot, goVersion)
if err != nil {
return fmt.Errorf("patch ast type check:%w", err)
}
Expand Down
104 changes: 79 additions & 25 deletions cmd/xgo/patch_compiler_ast_type_check.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package main

import "os"
import (
"os"

"github.com/xhd2015/xgo/support/goinfo"
)

const convertXY = `
if xgoConv, ok := x.expr.(*syntax.XgoSimpleConvert); ok {
Expand Down Expand Up @@ -57,37 +61,82 @@ if xgoConv, ok := x.expr.(*syntax.XgoSimpleConvert); ok {
}
`

func getExprInternalPatch(mark string, rawCall string, checkGoVersion func(goVersion *goinfo.GoVersion) bool) *Patch {
return &Patch{
Mark: mark,
InsertIndex: 5,
InsertBefore: true,
Anchors: []string{
`(check *Checker) exprInternal`,
"\n",
`default:`,
`case *syntax.Operation:`,
`case *syntax.KeyValueExpr:`,
`default:`,
"\n",
},
Content: `
case *syntax.XgoSimpleConvert:
kind := check.` + rawCall + `
x.expr = e
return kind
`,
CheckGoVersion: checkGoVersion,
}
}

var type2ExprPatch = &FilePatch{
FilePath: _FilePath{"src", "cmd", "compile", "internal", "types2", "expr.go"},
Patches: []*Patch{
getExprInternalPatch("type2_check_xgo_simple_convert", `rawExpr(nil, x, e.X, nil, false)`, func(goVersion *goinfo.GoVersion) bool {
return goVersion.Major > 1 || goVersion.Minor >= 21
}),
getExprInternalPatch("type2_check_xgo_simple_convert_no_target", `rawExpr(x, e.X, nil, false)`, func(goVersion *goinfo.GoVersion) bool {
return goVersion.Major == 1 && goVersion.Minor < 21
}),
{
Mark: "type2_check_xgo_simple_convert",
InsertIndex: 5,
InsertBefore: true,
Mark: "type2_match_type_xgo_simple_convert",
InsertIndex: 1,
Anchors: []string{
`(check *Checker) exprInternal`,
`func (check *Checker) matchTypes(x, y *operand) {`,
"\n",
`default:`,
`case *syntax.Operation:`,
`case *syntax.KeyValueExpr:`,
`default:`,
},
Content: convertXY,
CheckGoVersion: func(goVersion *goinfo.GoVersion) bool {
return goVersion.Major > 1 || goVersion.Minor >= 21
},
},
{
Mark: "type2_binary_convert_type_xgo_simple_convert",
InsertIndex: 2,
InsertBefore: true,
Anchors: []string{
`func (check *Checker) binary(x *operand`,
"\n",
`mayConvert := func(x, y *operand) bool {`,
},
Content: `
case *syntax.XgoSimpleConvert:
kind := check.rawExpr(nil, x, e.X, nil, false)
x.expr = e
return kind
`,
(func(x, y *operand){` + convertXY + `})(x,&y)
`,
CheckGoVersion: func(goVersion *goinfo.GoVersion) bool {
return goVersion.Major == 1 && goVersion.Minor >= 20 && goVersion.Minor < 21
},
},
{
Mark: "type2_match_type_xgo_simple_convert",
InsertIndex: 1,
Mark: "type2_binary_convert_type_xgo_simple_convert_can_mix",
InsertIndex: 2,
InsertBefore: true,
Anchors: []string{
`func (check *Checker) matchTypes(x, y *operand) {`,
`func (check *Checker) binary(x *operand`,
"\n",
`canMix := func(x, y *operand) bool {`,
},
Content: `
(func(x, y *operand){` + convertXY + `})(x,&y)
`,
CheckGoVersion: func(goVersion *goinfo.GoVersion) bool {
return goVersion.Major == 1 && goVersion.Minor < 20
},
Content: convertXY,
},
{
Mark: "type2_comparison_xgo_simple_convert",
Expand Down Expand Up @@ -192,25 +241,30 @@ type XgoSimpleConvert struct {
}
`

func patchCompilerAstTypeCheck(goroot string) error {
err := type2ExprPatch.Apply(goroot)
func patchCompilerAstTypeCheck(goroot string, goVersion *goinfo.GoVersion) error {
// always generate xgo_extra file
syntaxExtraFile := syntaxExtra.Join(goroot)
err := os.WriteFile(syntaxExtraFile, []byte(syntaxExtraPatch), 0755)
if err != nil {
return err
}
err = type2AssignmentsPatch.Apply(goroot)
if goVersion.Major == 1 && goVersion.Minor < 20 {
// only go1.20 and above supports const mock
return nil
}
err = type2ExprPatch.Apply(goroot, goVersion)
if err != nil {
return err
}
err = syntaxWalkPatch.Apply(goroot)
err = type2AssignmentsPatch.Apply(goroot, goVersion)
if err != nil {
return err
}
err = noderWriterPatch.Apply(goroot)
err = syntaxWalkPatch.Apply(goroot, goVersion)
if err != nil {
return err
}
syntaxExtraFile := syntaxExtra.Join(goroot)
err = os.WriteFile(syntaxExtraFile, []byte(syntaxExtraPatch), 0755)
err = noderWriterPatch.Apply(goroot, goVersion)
if err != nil {
return err
}
Expand Down
13 changes: 11 additions & 2 deletions cmd/xgo/patch_support.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package main

import "fmt"
import (
"fmt"

"github.com/xhd2015/xgo/support/goinfo"
)

type FilePatch struct {
FilePath _FilePath
Expand All @@ -17,9 +21,11 @@ type Patch struct {
Anchors []string

Content string

CheckGoVersion func(goVersion *goinfo.GoVersion) bool
}

func (c *FilePatch) Apply(goroot string) error {
func (c *FilePatch) Apply(goroot string, goVersion *goinfo.GoVersion) error {
if goroot == "" {
return fmt.Errorf("requires goroot")
}
Expand Down Expand Up @@ -52,6 +58,9 @@ func (c *FilePatch) Apply(goroot string) error {

return editFile(file, func(content string) (string, error) {
for _, patch := range c.Patches {
if patch.CheckGoVersion != nil && !patch.CheckGoVersion(goVersion) {
continue
}
beginMark := fmt.Sprintf("/*<begin %s>*/", patch.Mark)
endMark := fmt.Sprintf("/*<end %s>*/", patch.Mark)
content = addContentAtIndex(content, beginMark, endMark, patch.Anchors, patch.InsertIndex, patch.InsertBefore, patch.Content)
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.19"
const REVISION = "7fd3f2b160c52c890d248efceb23a5e070156e0c+1"
const NUMBER = 167
const REVISION = "55018f6d640a578b197c4a03220e7a3122326f62+1"
const NUMBER = 168

func getRevision() string {
revSuffix := ""
Expand Down
6 changes: 6 additions & 0 deletions patch/ctxt/ctxt_go1.20_and_above.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//go:build go1.20
// +build go1.20

package ctxt

const EnableTrapUntypedConst = true
6 changes: 6 additions & 0 deletions patch/ctxt/ctxt_non_go1.20.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//go:build !go1.20
// +build !go1.20

package ctxt

const EnableTrapUntypedConst = false
8 changes: 7 additions & 1 deletion patch/syntax/vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -595,8 +595,11 @@ func (c *BlockContext) trapValueNode(node *syntax.Name, globaleNames map[string]
// good to go
} else if decl.Kind == Kind_Const {
// untyped const(most cases) should only be used in
// serveral cases because runtime type is unknown
// several cases because runtime type is unknown
if decl.ConstDecl.Type == nil {
if !xgo_ctxt.EnableTrapUntypedConst {
return node
}
untypedConstType = getConstDeclValueType(decl.ConstDecl.Values)
var ok bool
explicitType, ok = c.isConstOKToTrap(node)
Expand Down Expand Up @@ -680,6 +683,9 @@ func (ctx *BlockContext) trapSelector(node syntax.Expr, sel *syntax.SelectorExpr
var untypedConstType string
if constInfo, ok := pkgData.Consts[sel.Sel.Value]; ok {
if constInfo.Untyped {
if !xgo_ctxt.EnableTrapUntypedConst {
return nil, true
}
untypedConstType = constInfo.Type
var ok bool
explicitType, ok = ctx.isConstOKToTrap(node)
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.19"
const REVISION = "7fd3f2b160c52c890d248efceb23a5e070156e0c+1"
const NUMBER = 167
const REVISION = "55018f6d640a578b197c4a03220e7a3122326f62+1"
const NUMBER = 168

// these fields will be filled by compiler
const XGO_VERSION = ""
Expand Down
61 changes: 61 additions & 0 deletions runtime/mock/MOCK_VAR_CONST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# API Summary
When passing a variable pointer to `Patch` and `Mock`, xgo will lookup for package level variables and setup trap for accessing to these variables.

Constant can only be patched via `PatchByName(pkg,name,replacer)`.

# Limitation
1. Only variables and consts of main module will be available for patching,
2. Constant patching requires go>=1.20.

# Examples
## `Patch` on variable
```go
package patch

import (
"testing"

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

var a int = 123

func TestPatchVarTest(t *testing.T) {
mock.Patch(&a, func() int {
return 456
})
b := a
if b != 456 {
t.Fatalf("expect patched variable a to be %d, actual: %d", 456, b)
}
}

```

Check [../test/patch_const/patch_var_test.go](../test/patch_const/patch_var_test.go) for more cases.

## `PatchByName` on constant
```go
package patch_const

import (
"testing"

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

const N = 50

func TestPatchInElseShouldWork(t *testing.T) {
mock.PatchByName("github.com/xhd2015/xgo/runtime/test/patch_const", "N", func() int {
return 5
})
b := N*4

if b != 20 {
t.Fatalf("expect b to be %d,actual: %d", 20, b)
}
}
```

Check [../test/patch_const/patch_const_test.go](../test/patch_const/patch_const_test.go) for more cases.
2 changes: 2 additions & 0 deletions runtime/mock/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# API Summary
> This document describes `Mock` and `Patch` for functions. For variables and consts, see [MOCK_VAR_CONST.md](./MOCK_VAR_CONST.md).
Mock exposes 3 `Mock` APIs to users:
- `Mock(fn, interceptor)` - for **99%** scenarios

Expand Down
Loading

0 comments on commit fe2e2f3

Please sign in to comment.