Go语言猴子补丁框架

Overview

Go语言猴子补丁框架 🙉 🐒

test workflow

Go 语言猴子补丁(monkey patching)框架。

本项目对 Bouke 的项目做了优化,不同协程可以独立 patch 同一个函数而互不影响。从而可以并发运行单元测试。

工作原理请参考我的系列文章:

Bouke 已经不再维护原项目,所以只能开一个新项目了 🤣

有兴趣的同学也可以加微信 taoshu-in 讨论,拉你进群。

快速入门

首先,引入 monkey 包

go get github.com/go-kiss/monkey

然后,调用 monkey.Patch 方法 mock 指定函数。

package main

import (
	"fmt"

	"github.com/go-kiss/monkey"
)

func sum(a, b int) int { return a + b }

func main() {
	monkey.Patch(sum, func(a b int) int { return a - b })
	fmt.Println(sum(1,2)) // 输出 -1
}

更多用法请参考使用示例测试用例

注意事项

  1. Monkey 需要关闭 Go 语言的内联优化才能生效,比如测试的时候需要:go test -gcflags=-l
  2. Monkey 需要在运行的时候修改内存代码段,因而无法在一些对安全性要求比较高的系统上工作。
  3. Monkey 不应该用于生产系统,但用来 mock 测试代码还是没有问题的。
  4. Monkey 目前仅支持 amd64 指令架构。支持 linux 和 macos。目前 windows 平台还有问题。
Comments
  • fix: window下支持

    fix: window下支持

    Windows

    TEXT github.com/huandu/go-tls/g.getg.abi0(SB) C:/Users/lzq/go/pkg/mod/github.com/huandu/[email protected]/g/getg_amd64.s
      getg_amd64.s:9	0x45ab80		65488b0c2528000000	MOVQ GS:0x28, CX	
      getg_amd64.s:10	0x45ab89		488b8100000000		MOVQ 0(CX), AX	// 需要在move一次	
      getg_amd64.s:11	0x45ab90		4889442408		MOVQ AX, 0x8(SP)	
      getg_amd64.s:12	0x45ab95		c3			RET			
    

    目前发现win下gs:0x28取出来其实是个二级指针,还需要再做一次取值才能和程序中的一致;我对汇编不是很熟,大致修改了一下,自己测试是可以的,大佬有更好的改法也可由大佬来改一下。

    opened by kkbblzq 4
  • 如何做到在patch状态下调用原函数

    如何做到在patch状态下调用原函数

    刚刚睡不着,画了大概10多分钟看了go夜读的 #119 Go monkey patch 的原理及应用 录播,发现都有一个问题对于类似这种情况见: https://github.com/cch123/supermonkey/issues/12 我看了原理以后感觉应该还是有办法能解决的,例如给开始加个汇编的判断,然后强制走回原函数,不过这个判断的值就比较麻烦了,如果引用一个全局变量倒是比较简单,但是这样频繁修改也不是太合理,要是能直接判断出父级方法是指定的方法然后调用原函数就好了。

    顺便放一下应用场景: https://blog.zeromake.com/pages/replace-now-offset/

    opened by zeromake 3
  • 结构体字段多导致 go test 卡住

    结构体字段多导致 go test 卡住

    // main.go
    package main
    
    type TreeNode struct {
    	Name     string
    	ID       int
    	Children []*TreeNode
    	A        int
    	B        int
    	C        int
    	// 注释掉 go test 可以正常运行
    	A1        int
    	B2        int
    	C1        int
    }
    
    func first[T any](a, b T) T { return a }
    
    func second[T any](a, b T) T { return b }
    
    // main_test.go
    package main
    
    import (
    	"fmt"
    	"reflect"
    	"testing"
    
    	"github.com/go-kiss/monkey"
    )
    
    func Test_main(t *testing.T) {
    	t1 := TreeNode{ ID: 1 }
    	t2 := TreeNode{ ID: 2 }
    
    	monkey.Patch(first[TreeNode], second[TreeNode], monkey.OptGeneric)
    	if reflect.DeepEqual(t2, first(t1, t2)) {
    		fmt.Println("equal")
    	} else {
    		fmt.Println("not equal")
    		t.Fatalf("not equal")
    	}
    }
    
    bug 
    opened by shijie152 1
  • 修复 struct 传值导致泛型打桩卡住的问题

    修复 struct 传值导致泛型打桩卡住的问题

    Fix #8

    Go 会为泛型函数生成中间函数。但实际需要 mock 的是中间函数所 调用的公共函数。为此,我们需要遍历中间函数机器码,通过 CALL 指令确定公共函数的地址。

    如果函数使用了 struct 值传递,Go 在一些场景下会插入若干额外 的 CALL 指令。这样 monkey 就会拿到错误的公共函数地址。

    为此,我们需要跳过因为 struct 传值而需要执行的 CALL 指令。

    通过观察生成的汇编代码,我发现这些 CALL 指令用 BP 寄存器,而 调用公共函数用的是 AX 寄存器。所以可以根据寄存器参数来过滤。

    opened by taoso 0
  • 重构 Patch 传参方式

    重构 Patch 传参方式

    最早只有一个 global 参数,后面又添加了 generic 参数。为了方便使用, 之前还直接提供了 PatchGlobal 函数。但支持泛型之后,就会产生四种组合, 没办法再提供快捷函数,索性搞了一个 PatchRaw 函数,让大家随便传参。

    但 PatchRaw 用起来非常麻烦,因为要为每一个参数都要提供对应的值,哪 怕不用也得传 fasle。这次通过引入 Option 接口来解决这个问题。

    因为给 Patch 函数添加了变长 Option 类型的参数,使用者可以按需组合 不同的参数来控制 Patch 的行为。

    opened by taoso 0
  • 重新支持全局打桩

    重新支持全局打桩

    本项目的亮点是分协程独立打桩。但今天发现一个必须使用全局打桩的场景。

    Go 标准 http 客户端在发起请求的时候会调用底层的 Dialer 创建连接。 但创建连接的过程是在另外一个独立的协程执行的。如果只允许协程内打桩, 那就无法通过 mock Dialer 来改变 http 客户端的行为。

    一种方式是 mock 更为上导致的 RoundTrip 对象。http 客户端对该对象的 操作是在同一个协程完成的。但作为通用库还是支持全局打桩比较好。

    这次顺便移除了 PatchInstanceMethod 函数,其功能完全可以由 Patch 实现。

    opened by taoso 0
  • 添加泛型支持

    添加泛型支持

    Go 在调用泛型函数的时候会为每一次的类型初始化生成不同的包装函数。 各包装函数会调用公共的底层函数。要想实现 mock,需要修改底层函数 代码段。

    但是 Go 并没有提供获取底层函数指针的机制。目前的办法是遍历包装 函数代码段,找到第一个 CALL 指令。再根据 CALL 指令的参数计算底层 函数的位置。

    因为支持泛型,所以 Go 最小支持版本为 1.18

    https://taoshu.in/go/monkey/generic.html

    opened by taoso 0
Owner
keep it simple, stupid
null