Giter Club home page Giter Club logo

xgo's Introduction

xgo

Go Reference Go Report Card Go Coverage CI Awesome Go

English | 简体中文

Enable function Trap in go, provide tools like Mock and Trace to help go developers write unit test and debug both easier and faster.

xgo works as a preprocessor for go run,go build, and go test(see our blog).

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.

These abilities include:

See Quick Start and Documentation for more details.

By the way, I promise you this is an interesting project.

Installation

go install github.com/xhd2015/xgo/cmd/xgo@latest

Verify the installation:

xgo version
# output:
#   1.0.x

If xgo is not found, you may need to check if $GOPATH/bin is added to your PATH variable.

For CI jobs like github workflow, see doc/INSTALLATION.md.

Requirement

xgo requires at least go1.17 to compile.

There is no specific limitation on OS and Architecture.

All OS and Architectures are supported by xgo as long as they are supported by go.

x86 x86_64 (amd64) arm64 any other Arch...
Linux Y Y Y Y
Windows Y Y Y Y
macOS Y Y Y Y
any other OS... Y Y Y Y

Quick Start

Let's write a unit test with xgo:

  1. Ensure you have installed xgo by following the Installation section, and verify the installation with:
xgo version
# output
#   1.0.x
  1. Init a go project:
mkdir demo
cd demo
go mod init demo
  1. Add demo_test.go with following code:
package demo_test

import (
	"testing"

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

func MyFunc() string {
	return "my func"
}

func TestFuncMock(t *testing.T) {
	mock.Patch(MyFunc, func() string {
		return "mock func"
	})
	text := MyFunc()
	if text != "mock func" {
		t.Fatalf("expect MyFunc() to be 'mock func', actual: %s", text)
	}
}
  1. Get the xgo/runtime dependency:
go get github.com/xhd2015/xgo/runtime
  1. Run the code:
# NOTE: xgo will take some time 
# for the first time to setup.
# It will be as fast as go after setup.
xgo test -v ./

Output:

=== RUN   TestFuncMock
--- PASS: TestFuncMock (0.00s)
PASS
ok      demo

If you run this with go, it would fail:

go test -v ./

Output:

WARNING: failed to link __xgo_link_on_init_finished(requires xgo).
WARNING: failed to link __xgo_link_on_goexit(requires xgo).
=== RUN   TestFuncMock
WARNING: failed to link __xgo_link_set_trap(requires xgo).
WARNING: failed to link __xgo_link_init_finished(requires xgo).
    demo_test.go:21: expect MyFunc() to be 'mock func', actual: my func
--- FAIL: TestFuncMock (0.00s)
FAIL
FAIL    demo    0.771s
FAIL

The above demo can be found at doc/demo.

API

Trap

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 for more details.)

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:

go run ./
# output:
#   A
#   B

Run with xgo:

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:

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.

Mock

Mock simplifies the process of setting up Trap interceptors.

API details: runtime/mock/README.md

The Mock API:

  • Mock(fn, interceptor)

Cheatsheet:

// 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,
  • 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,
  • Otherwise, the interceptor returns a non-nil error, that will be set to the function's return error.

There are other 2 APIs can be used to setup mock based on name, check runtime/mock/README.md for more details.

Method mock example:

type MyStruct struct {
    name string
}
func (c *MyStruct) Name() string {
    return c.name
}

func TestMethodMock(t *testing.T){
    myStruct := &MyStruct{
        name: "my struct",
    }
    otherStruct := &MyStruct{
        name: "other struct",
    }
    mock.Mock(myStruct.Name, func(ctx context.Context, fn *core.FuncInfo, args core.Object, results core.Object) error {
        results.GetFieldIndex(0).Set("mock struct")
        return nil
    })

    // myStruct is affected
    name := myStruct.Name()
    if name!="mock struct"{
        t.Fatalf("expect myStruct.Name() to be 'mock struct', actual: %s", name)
    }

    // otherStruct is not affected
    otherName := otherStruct.Name()
    if otherName!="other struct"{
        t.Fatalf("expect otherStruct.Name() to be 'other struct', actual: %s", otherName)
    }
}

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. If you want to mock additional stdlib functions, please file a discussion in Issue#6.

Patch

The runtime/mock package also provides another api:

  • Patch(fn,replacer) func()

Parameters:

  • fn same as described in 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:

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.

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

It is painful when debugging with a deep call stack.

Trace addresses this issue by collecting the hierarchical stack trace and stores it into file for later use.

Needless to say, with Trace, debug becomes less usual:

package trace_test

import (
    "fmt"
    "testing"
)

func TestTrace(t *testing.T) {
    A()
    B()
    C()
}

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

Run with xgo:

# run the test
# this will write the trace into TestTrace.json
# --strace represents stack trace
xgo test --strace ./

# view the trace
xgo tool trace TestTrace.json

Output: trace html

Another more complicated example from runtime/test/stack_trace/update_test.go: trace html

Real world examples:

Trace helps you get started to a new project quickly.

By default, Trace will write traces to a temp directory under current working directory. This behavior can be overridden by setting XGO_TRACE_OUTPUT to different values:

  • XGO_TRACE_OUTPUT=stdout: traces will be written to stdout, for debugging purpose,
  • XGO_TRACE_OUTPUT=<dir>: traces will be written to <dir>,
  • XGO_TRACE_OUTPUT=off: turn off trace.

Besides the --strace flag, xgo allows you to define which span should be collected, using trace.Begin():

import "github.com/xhd2015/xgo/runtime/trace"

func TestTrace(t *testing.T) {
    A()
    finish := trace.Begin()
    defer finish()
    B()
    C()
}

The trace will only include B() and C().

Concurrent safety

I know you guys from other monkey patching library suffer from the unsafety implied by these frameworks.

But I guarantee you mocking in xgo is builtin concurrent safe. That means, you can run multiple tests concurrently as long as you like.

Why? when you run a test, you setup some mock, these mocks will only affect the goroutine test running the test. And these mocks get cleared when the goroutine ends, no matter the test passed or failed.

Want to know why? Stay tuned, we are working on internal documentation.

Implementation Details

Working in progress...

See Issue#7 for more details.

This blog has a basic explanation: https://blog.xhd2015.xyz/posts/xgo-monkey-patching-in-go-using-toolexec

Why xgo?

The reason is simple: NO interface.

Yes, no interface, just for mocking. If the only reason to abstract an interface is to mock, then it only makes me feel boring, not working.

Extracting interface just for mocking is never an option to me. To the domain of the problem, it's merely a workaround. It enforces the code to be written in one style, that's why we don't like it.

Monkey patching simply does the right thing for the problem. But existing library are bad at compatibility.

So I created xgo, and hope it will finally take over other solutions to the mocking problem.

Comparing xgo with monkey

The project bouk/monkey, was initially created by bouk, as described in his blog https://bou.ke/blog/monkey-patching-in-go.

In short, it uses a low level assembly hack to replace function at runtime. Which exposes lots of confusing problems to its users as it gets used more and more widely(especially on macOS).

Then it was archived and no longer maintained by the author himself. However, two projects later take over the ASM idea and add support for newer go versions and architectures like Apple M1.

Still, the two does not solve the underlying compatibility issues introduced by ASM, including cross-platform support, the need to write to a read-only section of the execution code and lacking of general mock.

So developers still get annoying failures every now and then.

Xgo managed to solve these problems by avoiding low level hacking of the language itself. Instead, it relies on the IR representation employed by the go compiler.

It does a so-called IR Rewriting on the fly when the compiler compiles the source code. The IR(Intermediate Representation) is closer to the source code rather than the machine code. Thus it is much more stable than the monkey solution.

In conclusion, xgo and monkey are compared as the following:

xgo monkey
Technique IR ASM
Function Mock Y Y
Unexported Function Mock Y N
Per-Instance Method Mock Y N
Per-Goroutine Mock Y N
Per-Generic Type 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
API simple complex
Integration Effort easy hard

Contribution

Want to help contribute to xgo? Great! Check CONTRIBUTING for help.

Evolution of xgo

xgo is the successor of the original go-mock, which works by rewriting go code before compile.

The strategy employed by go-mock works well but causes much longer build time for larger projects due to source code explosion.

However, go-mock is remarkable for it's discovery of Trap, Trace besides Mock, and additional abilities like trapping variable and disabling map randomness.

It is the shoulder which xgo stands on.

xgo's People

Contributors

ccoveille avatar kilianc avatar xhd2015 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

xgo's Issues

Bug: comparing constants yeilds compile error

Code

const XGO_VERSION = ""

func TestListStdlib(t *testing.T) {
	if XGO_VERSION != "" {
		t.Fatalf("fail")
	}
}

Compile err:

cannot convert string(__xgo_XGO_VERSION_13_5) == "" (untyped bool value) to type string

Reason: during constant propagation, operations are wrapped also. But if the operator yields bool, we can stop.

fix typo in README

type MyStruct struct {
    name string
}
func (c *MyStruct) Name() name{
    return c.name
}

*MyStruct.Name return type is invalid, should be:

type MyStruct struct {
    name string
}
func (c *MyStruct) Name() string {
    return c.name
}

Fix: enable nested trap

Current implementation does not support nested mocking:

func Foo() string {
     return "foo " + Bar()
}

func Bar() string {
    return "bar"
}

If setup mock on Foo, then Bar cannot be mocked.

The main consideration is xgo cannot figure out if calling interceptor inside pre or post stage will cause infinite recursion or not. Actually, there are cases where calling or not calling can make sense. So xgo cannot determine if a function intercepting should be nested.

Thus, to avoid surprising users, xgo defaults to avoid nested trap.

Add github actions to auto run tests when push

The bug #9 (comment) was introduced by adding test on windows.

But when committing the tests only pass go1.22.1, from go1.17 to go1.21 were not tested against.

I believe if there were some CI job to do the tests this bug can be easily noticed and fixed.

Compile error: __xgo_autogen_register_func_info.go:9:85: constant 18446744073709551614 overflows int

When compiling:

const SET_EMPTY_STRING = "<empty>"
const SET_ZERO = math.MaxInt64 - 1
const SET_ZERO_UINT = math.MaxUint64 - 1
const SET_ZERO_INT32 = math.MaxInt32 - 1
const SET_ZERO_UINT32 = math.MaxUint32 - 1

Workaround:

const SET_EMPTY_STRING = "<empty>"
const SET_ZERO int64 = math.MaxInt64 - 1
const SET_ZERO_UINT uint64 = math.MaxUint64 - 1
const SET_ZERO_INT32 int32 = math.MaxInt32 - 1
const SET_ZERO_UINT32 uint32 = math.MaxUint32 - 1

Feature: convenient assert syntax

When writing asserts, could we just naturally do the actual check without repeating any name?

Current situation:

if result!="1234"{
    t.Fatalf("expect result to be %s, actual: %s", "123", result)
}

If we can just write:

assert.Assert(result=="1234")

And xgo prints the variable name and actual value in a breaking down style upon failure:

xxx_test.go:12    result=="1234"
                    ^
                    "123"

Besides, I think maybe we need to let user write AST plugins.

Bug: when building with `-overlay`, trap will not be generated

go.mod

module trace

go 1.21.7

require github.com/xhd2015/xgo/runtime v1.0.25

trace_test.go

package trace

import (
	"testing"

	_ "github.com/xhd2015/xgo/runtime/trace"
)

func TestTrace(t *testing.T) {
	A()
}

func A() string {
	return "A"
}

overlay.json

{
    "Replace": {
        "ROOT/trace/trace_test.go": "ROOT/tmp/trace_repalce_test.go"
    }
}

The replaced files content is identical to the original one.

run:

# trace normal
xgo test -v --strace ./

# trace empty
xgo test -v --strace --overlay overlay.json ./

Feature: compiler plugin

A general solution to #48

Provides the user a chance to inspect specific package's content, and modify them on the fly.

A possible implementation:

  • When xgo compiles a package, it finds possible matching plugins,
  • These plugins are go code which accepts a compile context as input, and return a Result describing what effects will be applied to the following compiling process, for instance, inserting a code snippet into beginning of a specific file

Plugin signature:

package plugin_demo

import (
     "github.com/xhd2015/xgo/plugin"
)

func Handle(ctx *plugin.CompileContext) *plugin.Result {
    return nil
}

Primary considerations:

  • Performance
    • the plugin should be an opt in feature, if there isn't any, then performance should be as same as before
    • if there is some, ensure only invoke them when targets match
  • Security
    • We should if user do not deliberately use it, then the compiler should bypass it
  • Reusability
    • Plugin can be written in another separate module, then imported by different projects.

Bug: when running tests for pocketbase, xgo v1.0.26 fails, while xgo v1.0.25 pass

Fail message:

fatal error: runtime: C malloc failed
_cgo_gotypes.go:202 ...
github.com/mattn/go-sqlite3._Cfunc_CString({0xc00004f130, 0x4a})
        _cgo_gotypes.go:202 +0x1d2 fp=0xc000e663d8 sp=0xc000e662c8 pc=0x101ac2a72
github.com/mattn/go-sqlite3.(*SQLiteDriver).Open(0xc0008eaf60, {0xc00004f130, 0x4a})
        /Users/xhd2015/Projects/gopath/pkg/mod/github.com/mattn/[email protected]/sqlite3.go:1456 +0x2a5e fp=0xc000e66920 sp=0xc000e663d8 pc=0x101af06be
database/sql.dsnConnector.Connect(...)
        /Users/xhd2015/.xgo/go-instrument/go1.21.7_Us_xh_in_go_096be049/go1.21.7/src/database/sql/sql.go:758

The line of code: name := C.CString(dsn).

After comparing xgo v1.0.26 with xgo v1.0.25, and do some bisect, found this is related to the new introduced --filepath:

fileValue := funcDecl.File   --> v1.0.26
fileValue := funcDecl.FileSyntax.Pos().RelFilename() --> v1.0.25

The actual fileValue for this package.

The stack trace shows that the file holding cgo types is _cgo_gotypes.go.

And by inserting a debug snippet into the go compiler:

relFile := f.Pos().RelFilename()
trimFile := TrimFilename(f.Pos().Base())
pkgPath := xgo_ctxt.GetPkgPath()
exec.Command("bash", "-c", fmt.Sprintf("echo 'pkg=%s relFile=%s trimFile=%s' >> /tmp/debug.log", pkgPath, relFile, trimFile)).Run()

The log shows:

pkg=github.com/mattn/go-sqlite3 relFile=/var/folders/y8/kmfy7f8s5bb5qfsp0z8h7j5m0000gq/T/go-build1192418818/b174/_cgo_gotypes.go trimFile=_cgo_gotypes.go

pkg=github.com/mattn/go-sqlite3 relFile=/var/folders/y8/kmfy7f8s5bb5qfsp0z8h7j5m0000gq/T/go-build1192418818/b174/_cgo_import.go trimFile=_cgo_import.go

Could be possibly introduced the difference between relFile and trimFile.
xgo v1.0.26: use trimFile
xgo v1.0.25: use relFile

windows run `xgo test` error

func TestMethodMock(t *testing.T) {
	myStruct := &MyStruct{
		name: "my struct",
	}
	otherStruct := &MyStruct{
		name: "other struct",
	}
	mock.Mock(myStruct.name, func(ctx context.Context, fn *core.FuncInfo, args core.Object, results core.Object) error {
		results.GetFieldIndex(0).Set("mock struct")
		return nil
	})

	// myStruct is affected
	name := myStruct.Name()
	if name != "mock struct" {
		t.Fatalf("expect myStruct.Name() to be 'mock struct', actual: %s", name)
	}

	// otherStruct is not affected
	otherName := otherStruct.Name()
	if otherName != "other struct" {
		t.Fatalf("expect otherStruct.Name() to be 'other struct', actual: %s", otherName)
	}
}

command: xgo test -run TestMethodMock .\mock_tcp_conn\
error: create ~/.xgo/log: mkdir C:\Users\admin\.xgo\go-instrument\go1.20.5_C:_Us_ad_sc_ap_go_cu_90061880: The directory name is invalid.

maybe create windows dir/file could not include ":"

Proposal: trap init functions

Currently init functions are skipped, that makes it impossible to inject mock and collect trace during the init stage.

However since init functions just run once and run serially, it has no performance impact at all. It might be useful.

Proposal: add a --trace, --trace=on|off to output trace instead of explicitly enable in each test case

Currently, if we want to visualize trace of each test ,we need to manually add a line:

func TestHelloWorld(t *testing.T){
    defer trace.Begin()()
    ...
}

But if we don't want trace, we need to manually remove that line, this causes some inconvenience.

So, propose adding a flag --trace flag, that will look like:

xgo test --trace -run TestHelloWorld

If --trace=off or not present, no trace will output.

Proposal: add support for mocking generic functions and methods

Generic functions and methods are currently skipped because their generic definition is just a template, there is no function instrumentation emitted until concrete types have been decided.

But since we use source code rewritting instead of IR rewritting for stdlib functions(see #6), it should be easy to also use source code rewritting for intercepting generic method and functions.

Generic function example:

package mock_generic_func

import (
	"context"
	"net/http"
	"testing"

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

func ToString[T any](v T) string {
    return fmt.Sprint(v)
}

func TestMockGenericFunc(t *testing.T) {
	mock.Mock(ToString[int], func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
		results.GetFieldIndex(0).Set("hello world")
		return nil
	})
        expect := "hello world"
        output := ToString(0)
	if expect != output {
		t.Fatalf("expect ToString[int](0) to be %s, actual: %s", expect, output)
	}
}

Generic method example:

package mock_generic_method

import (
	"context"
	"net/http"
	"testing"

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

type Formatter[T any] struct {
    prefix T
}

func (c *Formatter[T]) Format(v T) string {
    return fmt.Sprintf("%v: %v", c.prefix, v)
}

func TestMockGenericMethod(t *testing.T) {
       formatter := &Formatter[int]{ prefix: 1 }
	mock.Mock(formatter.Format, func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
		results.GetFieldIndex(0).Set("hello world")
		return nil
	})
        expect := "hello world"
        output := formatter.Format(0)
	if expect != output {
		t.Fatalf("expect ToString[int](0) to be %s, actual: %s", expect, output)
	}
}

What about MockByName and MockMethodByName ? Still works, the name can still be referenced without any type parameter.

Are we going to support per-type parameter mock? Seems no, but we can provide type parameter infos to the user to decide the concrete type. Or, user can just type assert the receiver or arguments or results to check what actual type parameter is.

Discussion: data driven testing

Never depend on the implementation directly. Abstract a layer between the implementation and the intent.

For example, when testing a service with external dependencies, we want to mock all of them.

But we should not directly mock their request or response, instead, we abstract a data structure called TestingContext.

This TestingContext is declared as a universal data fake storage:

type TestingContext struct {
    Users []*User
    Checkouts []*Checkout
    Config *Config
}

Then each dependency read data from this central data storage, and construct corresponding response based on the data.

And to reduce duplication of repeating declaring TestingContext, different tests can share a basic TestingContext, adding their custom overlay to the context.

Proposal: actively replacing gomonkey and mockery in open source projects

Since gomonkey and mockery both aim to provide mock for go, each in different ways. And their method should be considered outdated as xgo has evovled.

Thus, I propose we find active projects(has commits in last 2 months) that are still using these two library, finding how their tests are written with the help of the 2 library, and try to create a PR to replace them with xgo, demonstrating that xgo is a better choice, even the choice of the state of art.

Failed to build with if + defer closuer

xgo version: v1.0.10
go version: go 1.21

code

package demo

import (
	"testing"
)

const condition = false

func TestClosuerWithIf(t *testing.T) {
	t.Logf("begin \n")
	if condition {
		defer func() {
				t.Logf("defer\n")
		}()
	}

	t.Logf("end\n")
}

error:

$ xgo test ./demo
sub.TestClosuerWithIf.func1·f: relocation target sub.TestClosuerWithIf.func1 not defined
FAIL   demo [build failed]

However, without the if condition, everything works fine.
With go1.22, the above code works fine.

Proposal: add support for mocking unexported functions

There are cases where some function are just not exported by a function, by we really want to bypass them.

The ability to mock should be not bound to visibility.

Example:

package provider

func DoSomething(...){
    ...
   doSomethingNetwork(...)
    ...
}

func doSomethingNetwork(...){
}

If we must rely on DoSomething but cannot change its code, then unless doSomethingNetwork can be mocked, otherwise we cannot write a test case that bypasses it.

The API would look like:

// package level function
func MockByName(pkg string, funcName string,interceptor func() error)) func()

// instance level emthod
// NOTE: instance must have method
func MockMethodByName(instance interface{}, method string, interceptor func() error) func()

Usage:

mock.MockByName("github.com/xhd2015/xgo/test", "doSomethingNetwork", func(ctx context.Context) error{
     // bypass the private function
     return nil
})

Stdlib: add supports for encoding/json

Specifically, the json.newTypeEncoder. When it returns unsupported function, we can just silently ignore that field.

Here is an example when trying to marshal a stack trace with cancelFunc unmarshalable:

error:json: error calling MarshalJSON for type *sorted_map.SortedMap: json: error calling MarshalJSON for type *sorted_map.SortedMap: json: error calling MarshalJSON for type *sorted_map.SortedMap: json: error calling MarshalJSON for type *sorted_map.SortedMap: json: error calling MarshalJSON for type trap.object: field _r1: json: unsupported type: context.CancelFunc

run test failed in WSL

I download xgo by install.sh

curl -fsSL https://github.com/xhd2015/xgo/raw/master/install.sh | bash

And when I run:

xgo test -run TestMethodMock ./mock_tcp_conn

I got error:

fork/exec /Users/xhd2015/installed/go1.21.7/bin/go: no such file or directory

Maybe something wrong with build-release

Add detailed internal design document

The so-called IR rewriting sounds unfamiliar to people.What's it? How does it solve the problem that go-monkey suffers?How does it compare to other mock strategies?Can we dig further to employ this technique not only in mock, but also system tracing?

Proposal: add tls package

TLS is useful when context is not passing everywhere. And I don't want people to change their code to just add context just for passing goroutine local data

Proposal: a new generic based API

Currently the mock.Mock(fn,interceptor) API provides a general way to setup mock.

When testing a logic with many external dependencies, the mock setup ends with a lot of results.GetFieldIndex(0).Set(...) call, as below:

func TestMyLargeFunc(t *testing.T){
    mock.Mock(pkg1.F, func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
		return nil
    })
    mock.Mock(pkg2.F, func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
                results.GetFieldIndex(0).Set(&Model{...})
		return nil
    })
    mock.Mock(pkg3.F, func(ctx context.Context, fn *core.FuncInfo, args, results core.Object) error {
                results.GetFieldIndex(0).Set(&Model{...})
		return nil
    })
    ...
    MyLargeFunc()
    ...
}

Thus, we propose a new API based on generic:

// T should be a function
func Patch[T](fn T, replacer T) func()

The above test can be write as following with better type safety:

func TestMyLargeFunc(t *testing.T){
    mock.Patch(pkg1.F, func() error {
		return nil
    })
    mock.Patch(pkg2.F, func(ctx context.Context) (*Model,error){
                return &Model{...},nil
    })
    mock.Patch(pkg3.F, func(ctx context.Context) (*Model,error){
                return &Model{...},nil
    })
    ...
    MyLargeFunc()
    ...
}

As for go1.17 and below, the function can just be:

// fn should be a function
func Patch(fn interface{}, replacer interface{}) func()

It should be compatible with the generic version.

As for the mock by name version, also add corresponding PatchByName and PatchMethodByName, but no generic param can be specified. And it is the user's responsibility to ensure the function signature matches.

NOTE: this Patch is not always feasible if some type in the function signature are not exported. In these case, just use the Mock API as before.

Bug: fatal error: semacquire not on the G stack

fatal error: semacquire not on the G stack

runtime stack:
sync.runtime_SemacquireMutex(0x70000fd9ed90, 0x6b, 0x70000fd9edc8)
        /Users/xhd2015/.xgo/go-instrument/go1.17.5_Us_xh_in_go_8887a105/go1.17.5/src/runtime/sema.go:71 +0x25

The code that causes panic (https://github.com/xhd2015/xgo/blob/master/runtime/trap/trap.go):

func init() {
	__xgo_link_on_gonewproc(func(g uintptr) {
		if isByPassing() {
			return
		}
		local := getLocalInterceptorList()
		if local == nil || (len(local.head) == 0 && len(local.tail) == 0) {
			return
		}
		// inherit interceptors of last group
		localInterceptors.Store(g, &interceptorGroup{   <---- localInterceptors.mu.Lock() causes the panic
			groups: []*interceptorList{{
				list: local.copy(),
			}},
		})
	})
}

Feature: allow supressing some package init logics

When running unit test, some packages are annoying.Though we are not going to use it while testing, it still stops testing.

Example: vendor/github.com/lucas-clemente/quic-go/internal/handshake/unsafe.go

func init() {
	if !structsEqual(&tls.ConnectionState{}, &qtls.ConnectionState{}) {
		panic("qtls.ConnectionState not compatible with tls.ConnectionState")
	}
	if !structsEqual(&tls.ClientSessionState{}, &qtls.ClientSessionState{}) {
		panic("qtls.ClientSessionState not compatible with tls.ClientSessionState")
	}
}

I want to rewrite it into

func init() {
        if true {
            return
        }
	if !structsEqual(&tls.ConnectionState{}, &qtls.ConnectionState{}) {
		panic("qtls.ConnectionState not compatible with tls.ConnectionState")
	}
	if !structsEqual(&tls.ClientSessionState{}, &qtls.ClientSessionState{}) {
		panic("qtls.ClientSessionState not compatible with tls.ClientSessionState")
	}
}

Bug: var mock may cause sync.Mutex to be copied

var lock sync.Mutex

func demo(){
     lock.Lock()
    defer lock.Unlock()
     ...
}

Will be transformed to:

var lock sync.Mutex

func demo(){
    __xgo_lock1 := lock
    __xgo_lock1.Lock()
    __xgo_lock2 := lock
     defer __xgo_lock2.Unlock()
     ...
}

Is obviously wrong.

Other types include: sync.Map.

Proposal: skip generated file when debugging

Given a test:

package debug

import (
	"testing"

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

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

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

When debugging with delve, we set a breakpoint at the line the calls mock.Patch, then click step into, we will enter a file named __xgo_autogen_register_func_helper.go

The call stack looks like:

[debug.__xgo_link_generated_init_regs_body] debug_test.go    <--- stack top
[debug.init.0]  __xgo_autogen_register_func_helper.go
[runtime.doInit1] proc.go
[runtime.doInit] proc.go
[runtime.main] proc.go
[runtime.goexit] asm_amd64.s

After stepping into, the call stack looks like:

[debug.__xgo_local_register_func]   __xgo_autogen_register_func_helper.go  <--- stack top
[debug.__xgo_link_generated_init_regs_body] debug_test.go
[debug.init.0]  __xgo_autogen_register_func_helper.go
[runtime.doInit1] proc.go
[runtime.doInit] proc.go
[runtime.main] proc.go
[runtime.goexit] asm_amd64.s

And an error prompted by vscode saying:The editor could not be opened because the file was not found

The file path shows runtime/test/__xgo_autogen_register_func_helper.go.

Demo:
image

json is not being generated :-(

This is my command:

xgo test -v -run TestTodoController_Suite --strace ./interfaces/controllers

And this is my output:
Notice the warnings at the top....

go: -replace=../MHGoLib=/Users/marcelloh/data/Go/go-private/SBC/MHGoLib: invalid old path: malformed import path "../MHGoLib": invalid path element ".."
WARNING: --strace requires: import _ "github.com/xhd2015/xgo/runtime/trace"
   failed to auto import github.com/xhd2015/xgo/runtime/trace: exit status 1
=== RUN   TestTodoController_Suite
=== RUN   TestTodoController_Suite/Test_TodoController_EditRecord_Error_ID
=== RUN   TestTodoController_Suite/Test_TodoController_EditRecord_Good
=== RUN   TestTodoController_Suite/Test_TodoController_EditRecord_Wrong
=== RUN   TestTodoController_Suite/Test_TodoController_GetMoreRecords
=== RUN   TestTodoController_Suite/Test_TodoController_GetMoreRecordsJSON
=== RUN   TestTodoController_Suite/Test_TodoController_GetMoreRecordsJSON_Error
=== RUN   TestTodoController_Suite/Test_TodoController_GetOneRecord
=== RUN   TestTodoController_Suite/Test_TodoController_GetOneRecordJSON_Error_ID
=== RUN   TestTodoController_Suite/Test_TodoController_GetOneRecordJSON_Error_Wrong
=== RUN   TestTodoController_Suite/Test_TodoController_GetOneRecordJSON_Good
=== RUN   TestTodoController_Suite/Test_TodoController_GetOneRecord_Error_ID
=== RUN   TestTodoController_Suite/Test_TodoController_GetOneRecord_Error_Wrong
=== RUN   TestTodoController_Suite/Test_TodoController_SaveRecord_Error_ID
=== RUN   TestTodoController_Suite/Test_TodoController_SaveRecord_Good
=== RUN   TestTodoController_Suite/Test_TodoController_SaveRecord_Wrong
=== RUN   TestTodoController_Suite/Test_TodoController_SaveRecord_WrongValidation
--- PASS: TestTodoController_Suite (0.03s)
    --- PASS: TestTodoController_Suite/Test_TodoController_EditRecord_Error_ID (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_EditRecord_Good (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_EditRecord_Wrong (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_GetMoreRecords (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_GetMoreRecordsJSON (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_GetMoreRecordsJSON_Error (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_GetOneRecord (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_GetOneRecordJSON_Error_ID (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_GetOneRecordJSON_Error_Wrong (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_GetOneRecordJSON_Good (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_GetOneRecord_Error_ID (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_GetOneRecord_Error_Wrong (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_SaveRecord_Error_ID (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_SaveRecord_Good (0.01s)
    --- PASS: TestTodoController_Suite/Test_TodoController_SaveRecord_Wrong (0.00s)
    --- PASS: TestTodoController_Suite/Test_TodoController_SaveRecord_WrongValidation (0.00s)
PASS
ok      dev.local/marcelloh/SBC/interfaces/controllers    0.520s

Tool: a playground for xgo

Add a playground to run examples lively, and show traces by running some tests.

That may probably introduce a domain for xgo itself.

Windows Friendly Support

  1. add windows install powershell like install.ps1
  2. build from source with windows met two errors:
    1. without "--local" options, goterror: sum sha256: exec: "shasum": executable file not found in %PATH%
    2. with "--local" options, got error: GOOS=windows GOARCH=amd64:rename C:\Users\admin\AppData\Local\Temp\xgo-release3949503335\xgo C:\Users\admin\.xgo\bin\xgo: The system cannot find the path specified.

Additionally, build from source binary with windows without ".exe" suffix

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.