From aed8ed570352394749b34f7ddc759a7a67cc143b Mon Sep 17 00:00:00 2001 From: xhd2015 Date: Mon, 8 Apr 2024 00:04:40 +0800 Subject: [PATCH] make constant mock available for go1.20 and above --- README.md | 6 +- README_zh_cn.md | 4 + cmd/xgo/patch_compiler.go | 2 +- cmd/xgo/patch_compiler_ast_type_check.go | 104 +++++++++++++----- cmd/xgo/patch_support.go | 13 ++- cmd/xgo/version.go | 4 +- patch/ctxt/ctxt_go1.20_and_above.go | 6 + patch/ctxt/ctxt_non_go1.20.go | 6 + patch/syntax/vars.go | 6 + runtime/core/version.go | 4 +- runtime/mock/MOCK_VAR_CONST.md | 61 ++++++++++ runtime/mock/README.md | 2 + runtime/test/patch/patch_var_test.go | 18 --- runtime/test/patch/sub/sub.go | 2 - .../patch_const_test.go | 30 ++++- runtime/test/patch_const/stub.go | 1 + runtime/test/patch_const/sub/sub.go | 3 + script/run-test/main.go | 1 + 18 files changed, 215 insertions(+), 58 deletions(-) create mode 100644 patch/ctxt/ctxt_go1.20_and_above.go create mode 100644 patch/ctxt/ctxt_non_go1.20.go create mode 100644 runtime/mock/MOCK_VAR_CONST.md rename runtime/test/{patch => patch_const}/patch_const_test.go (89%) create mode 100644 runtime/test/patch_const/stub.go create mode 100644 runtime/test/patch_const/sub/sub.go diff --git a/README.md b/README.md index 7960039b..179709e7 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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| diff --git a/README_zh_cn.md b/README_zh_cn.md index c2ce53a9..737b62ab 100644 --- a/README_zh_cn.md +++ b/README_zh_cn.md @@ -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 在调试一个非常深的调用栈时, 通常会感觉非常痛苦, 并且效率低下。 @@ -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| diff --git a/cmd/xgo/patch_compiler.go b/cmd/xgo/patch_compiler.go index 5e921bd5..00baf12d 100644 --- a/cmd/xgo/patch_compiler.go +++ b/cmd/xgo/patch_compiler.go @@ -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) } diff --git a/cmd/xgo/patch_compiler_ast_type_check.go b/cmd/xgo/patch_compiler_ast_type_check.go index 94c77d2b..04fb9009 100644 --- a/cmd/xgo/patch_compiler_ast_type_check.go +++ b/cmd/xgo/patch_compiler_ast_type_check.go @@ -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 { @@ -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", @@ -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 } diff --git a/cmd/xgo/patch_support.go b/cmd/xgo/patch_support.go index 9fc3edd9..43d13714 100644 --- a/cmd/xgo/patch_support.go +++ b/cmd/xgo/patch_support.go @@ -1,6 +1,10 @@ package main -import "fmt" +import ( + "fmt" + + "github.com/xhd2015/xgo/support/goinfo" +) type FilePatch struct { FilePath _FilePath @@ -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") } @@ -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("/**/", patch.Mark) endMark := fmt.Sprintf("/**/", patch.Mark) content = addContentAtIndex(content, beginMark, endMark, patch.Anchors, patch.InsertIndex, patch.InsertBefore, patch.Content) diff --git a/cmd/xgo/version.go b/cmd/xgo/version.go index 83a09036..85b074b9 100644 --- a/cmd/xgo/version.go +++ b/cmd/xgo/version.go @@ -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 := "" diff --git a/patch/ctxt/ctxt_go1.20_and_above.go b/patch/ctxt/ctxt_go1.20_and_above.go new file mode 100644 index 00000000..90cee92c --- /dev/null +++ b/patch/ctxt/ctxt_go1.20_and_above.go @@ -0,0 +1,6 @@ +//go:build go1.20 +// +build go1.20 + +package ctxt + +const EnableTrapUntypedConst = true diff --git a/patch/ctxt/ctxt_non_go1.20.go b/patch/ctxt/ctxt_non_go1.20.go new file mode 100644 index 00000000..31df4c29 --- /dev/null +++ b/patch/ctxt/ctxt_non_go1.20.go @@ -0,0 +1,6 @@ +//go:build !go1.20 +// +build !go1.20 + +package ctxt + +const EnableTrapUntypedConst = false diff --git a/patch/syntax/vars.go b/patch/syntax/vars.go index 550d5d07..d062ca0f 100644 --- a/patch/syntax/vars.go +++ b/patch/syntax/vars.go @@ -597,6 +597,9 @@ func (c *BlockContext) trapValueNode(node *syntax.Name, globaleNames map[string] // untyped const(most cases) should only be used in // serveral 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) @@ -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) diff --git a/runtime/core/version.go b/runtime/core/version.go index 74296c39..26d9cb74 100644 --- a/runtime/core/version.go +++ b/runtime/core/version.go @@ -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 = "" diff --git a/runtime/mock/MOCK_VAR_CONST.md b/runtime/mock/MOCK_VAR_CONST.md new file mode 100644 index 00000000..d5f90783 --- /dev/null +++ b/runtime/mock/MOCK_VAR_CONST.md @@ -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 varaibel 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. \ No newline at end of file diff --git a/runtime/mock/README.md b/runtime/mock/README.md index 715324e5..747926a4 100644 --- a/runtime/mock/README.md +++ b/runtime/mock/README.md @@ -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 diff --git a/runtime/test/patch/patch_var_test.go b/runtime/test/patch/patch_var_test.go index ec31a075..8edf2991 100644 --- a/runtime/test/patch/patch_var_test.go +++ b/runtime/test/patch/patch_var_test.go @@ -3,7 +3,6 @@ package patch import ( "encoding/json" "fmt" - "os" "testing" "github.com/xhd2015/xgo/runtime/mock" @@ -93,23 +92,6 @@ func toJSONRaw(v interface{}) (json.RawMessage, error) { } } -const a3 = 4 - -func TestPatchInElseShouldWork(t *testing.T) { - if os.Getenv("nothing") == "nothing" { - t.Fatalf("should go else") - } else { - mock.PatchByName(pkgPath, "a3", func() int { - return 5 - }) - b := a3 - - if b != 5 { - t.Fatalf("expect b to be %d,actual: %d", 5, b) - } - } -} - func TestMakeInOtherPackageShouldCompile(t *testing.T) { // previous error:sub.NameSet (type) is not an expression set := make(sub.NameSet) diff --git a/runtime/test/patch/sub/sub.go b/runtime/test/patch/sub/sub.go index cbc28085..1ac05321 100644 --- a/runtime/test/patch/sub/sub.go +++ b/runtime/test/patch/sub/sub.go @@ -1,5 +1,3 @@ package sub -const N = 50 - type NameSet map[string]bool diff --git a/runtime/test/patch/patch_const_test.go b/runtime/test/patch_const/patch_const_test.go similarity index 89% rename from runtime/test/patch/patch_const_test.go rename to runtime/test/patch_const/patch_const_test.go index 98307736..71abd26f 100644 --- a/runtime/test/patch/patch_const_test.go +++ b/runtime/test/patch_const/patch_const_test.go @@ -1,4 +1,7 @@ -package patch +//go:build go1.20 +// +build go1.20 + +package patch_const import ( "fmt" @@ -7,11 +10,30 @@ import ( "unsafe" "github.com/xhd2015/xgo/runtime/mock" - "github.com/xhd2015/xgo/runtime/test/patch/sub" + "github.com/xhd2015/xgo/runtime/test/patch_const/sub" ) +const pkgPath = "github.com/xhd2015/xgo/runtime/test/patch_const" +const subPkgPath = "github.com/xhd2015/xgo/runtime/test/patch_const/sub" const testVersion = "1.0" +const N = 50 + +func TestPatchInElseShouldWork(t *testing.T) { + if os.Getenv("nothing") == "nothing" { + t.Fatalf("should go else") + } else { + mock.PatchByName(pkgPath, "N", func() int { + return 5 + }) + b := N + + if b != 5 { + t.Fatalf("expect b to be %d,actual: %d", 5, b) + } + } +} + func TestPatchConstByNamePtrTest(t *testing.T) { mock.PatchByName(pkgPath, "testVersion", func() string { return "1.5" @@ -28,7 +50,7 @@ func TestPatchConstByNameWrongTypeShouldFail(t *testing.T) { defer func() { pe = recover() }() - mock.PatchByName(pkgPath, "a", func() string { + mock.PatchByName(pkgPath, "N", func() string { return "1.5" }) }() @@ -42,8 +64,6 @@ func TestPatchConstByNameWrongTypeShouldFail(t *testing.T) { } } -const N = 50 - func TestPatchConstOperationShouldCompileAndSkipMock(t *testing.T) { // should have effect mock.PatchByName(pkgPath, "N", func() int { diff --git a/runtime/test/patch_const/stub.go b/runtime/test/patch_const/stub.go new file mode 100644 index 00000000..acb40e31 --- /dev/null +++ b/runtime/test/patch_const/stub.go @@ -0,0 +1 @@ +package patch_const diff --git a/runtime/test/patch_const/sub/sub.go b/runtime/test/patch_const/sub/sub.go new file mode 100644 index 00000000..8348c677 --- /dev/null +++ b/runtime/test/patch_const/sub/sub.go @@ -0,0 +1,3 @@ +package sub + +const N = 50 diff --git a/script/run-test/main.go b/script/run-test/main.go index 7243c2a9..2a690208 100644 --- a/script/run-test/main.go +++ b/script/run-test/main.go @@ -47,6 +47,7 @@ var runtimeSubTests = []string{ "mock_var", "trap_args", "patch", + "patch_const", } func main() {