您的位置:首页 > 教程 > 其他脚本 > Go语言开发代码自测绝佳go fuzzing用法详解

Go语言开发代码自测绝佳go fuzzing用法详解

2022-06-21 14:06:35 来源:易采站长站 作者:

Go语言开发代码自测绝佳go fuzzing用法详解

目录
特别说明go fuzzing 是什么go fuzzing 怎么用运行 fuzzing testsgo-zero 的最佳实践1. 定义 fuzzing arguments2. 怎么写 fuzzing target3. 失败 case 如何打印输入4. 编写新的测试用例go fuzzing 更多经验Go 版本问题go fuzzing 不能复现的失败复杂用法示例

fwy站长之家-易采站长站-Easck.Com

特别说明

这个真的不是标题党,我写代码20+年,真心认为>

go fuzzing 至今已经发现了代码质量极高的 Go 标准库超过200个bug,见:github.com/dvyukov/go-…fwy站长之家-易采站长站-Easck.Com

春节程序员之间的祝福经常是,祝你代码永无 bug!虽然调侃,但对我们每个程序员来说,每天都在写 bug,这是事实。代码没 bug 这事,只能证伪,不能证明。即将发布的 Go 1.18 官方提供了一个帮助我们证伪的绝佳工具 - go fuzzing。fwy站长之家-易采站长站-Easck.Com

Go 1.18 大家最关注的是泛型,然而我真的觉得 go fuzzing 真的是 Go 1.18 最有用的功能,没有之一!fwy站长之家-易采站长站-Easck.Com

本文我们就来详细看看 go fuzzing:fwy站长之家-易采站长站-Easck.Com

    是什么?怎么用?有何最佳实践?

    首先,你需要升级到 Go 1.18fwy站长之家-易采站长站-Easck.Com

    Go 1.18 虽然还未正式发布,但你可以下载 RC 版本,而且即使你生产用 Go 更早版本,你也可以开发环境使用 go fuzzing 寻找 bugfwy站长之家-易采站长站-Easck.Com

    fwy站长之家-易采站长站-Easck.Com

    go>

    根据 官方文档 介绍,go fuzzing 是通过持续给一个程序不同的输入来自动化测试,并通过分析代码覆盖率来智能的寻找失败的 case。这种方法可以尽可能的寻找到一些边缘 case,亲测确实发现的都是些平时很难发现的问题。fwy站长之家-易采站长站-Easck.Com

    fwy站长之家-易采站长站-Easck.Com

    fwy站长之家-易采站长站-Easck.Com

    go>

    官方介绍写 fuzz tests 的一些规则:fwy站长之家-易采站长站-Easck.Com

      函数必须是 Fuzz开头,唯一的参数是 *testing.F,没有返回值Fuzz tests 必须在 *_test.go 的文件里上图中的 fuzz target 是个方法调用 (*testing.F).Fuzz,第一个参数是 *testing.T,然后就是称之为 fuzzing arguments 的参数,没有返回值每个 fuzz test 里只能有一个 fuzz target调用 f.Add(…) 的时候需要参数类型跟 fuzzing arguments 顺序和类型都一致

      fuzzing arguments 只支持以下类型:fwy站长之家-易采站长站-Easck.Com

      string, []bytefwy站长之家-易采站长站-Easck.Com

      int, int8, int16, int32/rune, int64fwy站长之家-易采站长站-Easck.Com

      uint, uint8/byte, uint16, uint32, uint64fwy站长之家-易采站长站-Easck.Com

      float32, float64fwy站长之家-易采站长站-Easck.Com

      boolfwy站长之家-易采站长站-Easck.Com

      fuzz target 不要依赖全局状态,会并行跑。fwy站长之家-易采站长站-Easck.Com

      fwy站长之家-易采站长站-Easck.Com

      运行>

      如果我写了一个 fuzzing test,比如:fwy站长之家-易采站长站-Easck.Com

      // 具体代码见 https://github.com/zeromicro/go-zero/blob/master/core/mr/mapreduce_fuzz_test.go
      func FuzzMapReduce(f *testing.F) {
        ...
      }
      

      那么我们可以这样执行:fwy站长之家-易采站长站-Easck.Com

      go test -fuzz=MapReduce
      

      我们会得到类似如下结果:fwy站长之家-易采站长站-Easck.Com

      fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed
      fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers
      fuzz: elapsed: 3s, execs: 3338 (1112/sec), new interesting: 56 (total: 57)
      fuzz: elapsed: 6s, execs: 6770 (1144/sec), new interesting: 62 (total: 63)
      fuzz: elapsed: 9s, execs: 10157 (1129/sec), new interesting: 69 (total: 70)
      fuzz: elapsed: 12s, execs: 13586 (1143/sec), new interesting: 72 (total: 73)
      ^Cfuzz: elapsed: 13s, execs: 14031 (1084/sec), new interesting: 72 (total: 73)
      PASS
      ok    github.com/zeromicro/go-zero/core/mr  13.169s
      

      其中的 ^C 是我按了 ctrl-C 终止了测试,详细解释参考官方文档。fwy站长之家-易采站长站-Easck.Com

      fwy站长之家-易采站长站-Easck.Com

      go-zero>

      按照我使用下来的经验总结,我把最佳实践初步总结为以下四步:fwy站长之家-易采站长站-Easck.Com

        定义 fuzzing arguments,首先要想明白怎么定义 fuzzing arguments,并通过给定的 fuzzing arguments 写 fuzzing target思考 fuzzing target 怎么写,这里的重点是怎么验证结果的正确性,因为 fuzzing arguments 是“随机”给的,所以要有个通用的结果验证方法思考遇到失败的 case 如何打印结果,便于生成新的 unit test根据失败的 fuzzing test 打印结果编写新的 unit test,这个新的 unit test会被用来调试解决fuzzing test发现的问题,并固化下来留给CI 用

        接下来我们以一个最简单的数组求和函数来展示一下上述步骤,go-zero 的实际案例略显复杂,文末我会给出 go-zero 内部落地案例,供大家参考复杂场景写法。fwy站长之家-易采站长站-Easck.Com

        这是一个注入了 bug 的求和的代码实现:fwy站长之家-易采站长站-Easck.Com

        func Sum(vals []int64) int64 {
          var total int64
          for _, val := range vals {
            if val%1e5 != 0 {
              total += val
            }
          }
          return total
        }
        

        fwy站长之家-易采站长站-Easck.Com

        1.>

        你至少需要给出一个 fuzzing argument,不然 go fuzzing 没法生成测试代码,所以即使我们没有很好的输入,我们也需要定义一个对结果产生影响的 fuzzing argument,这里我们就用 slice 元素个数作为 fuzzing arguments,然后 Go fuzzing 会根据跑出来的 code coverage 自动生成不同的参数来模拟测试。fwy站长之家-易采站长站-Easck.Com

        func FuzzSum(f *testing.F) {
          f.Add(10)
          f.Fuzz(func(t *testing.T, n int) {
            n %= 20
            ...
          })
        }
        

        这里的 n 就是让 go fuzzing 来模拟 slice 元素个数,为了保证元素个数不会太多,我们限制在20以内(0个也没问题),并且我们添加了一个值为10的语料(go fuzzing 里面称之为 corpus),这个值就是让 go fuzzing 冷启动的一个值,具体为多少不重要。fwy站长之家-易采站长站-Easck.Com

        fwy站长之家-易采站长站-Easck.Com

        2.>

        这一步的重点是如何编写可验证的 fuzzing target,根据给定的 fuzzing arguments 写出测试代码的同时,还需要生成验证结果正确性用的数据。fwy站长之家-易采站长站-Easck.Com

        对我们这个 Sum 函数来说,其实还是比较简单的,就是随机生成 n 个元素的 slice,然后求和算出期望的结果。如下:fwy站长之家-易采站长站-Easck.Com

        func FuzzSum(f *testing.F) {
          rand.Seed(time.Now().UnixNano())
          f.Add(10)
          f.Fuzz(func(t *testing.T, n int) {
            n %= 20
            var vals []int64
            var expect int64
            for i := 0; i < n; i++ {
              val := rand.Int63() % 1e6
              vals = append(vals, val)
              expect += val
            }
            assert.Equal(t, expect, Sum(vals))
          })
        }
        

        这段代码还是很容易理解的,自己求和和 Sum 求和做比较而已,就不详细解释了。但复杂场景你就需要仔细想想怎么写验证代码了,不过这不会太难,太难的话,可能是对测试函数没有足够理解或者简化。fwy站长之家-易采站长站-Easck.Com

        此时就可以用如下命令跑 fuzzing tests 了,结果类似如下:fwy站长之家-易采站长站-Easck.Com

        $ go test -fuzz=Sum
        fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed
        fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers
        fuzz: elapsed: 0s, execs: 6672 (33646/sec), new interesting: 7 (total: 6)
        --- FAIL: FuzzSum (0.21s)
            --- FAIL: FuzzSum (0.00s)
                sum_fuzz_test.go:34:
                      Error Trace:  sum_fuzz_test.go:34
                                          value.go:556
                                          value.go:339
                                          fuzz.go:334
                      Error:        Not equal:
                                    expected: 8736932
                                    actual  : 8636932
                      Test:         FuzzSum
            Failing input written to testdata/fuzz/FuzzSum/739002313aceff0ff5ef993030bbde9115541cabee2554e6c9f3faaf581f2004
            To re-run:
            go test -run=FuzzSum/739002313aceff0ff5ef993030bbde9115541cabee2554e6c9f3faaf581f2004
        FAIL
        exit status 1
        FAIL  github.com/kevwan/fuzzing  0.614s
        

        那么问题来了!我们看到了结果不对,但是我们很难去分析为啥不对,你仔细品品,上面这段输出,你怎么分析?fwy站长之家-易采站长站-Easck.Com

        fwy站长之家-易采站长站-Easck.Com

        3.>

        对于上面失败的测试,我们如果能打印出输入,然后形成一个简单的测试用例,那我们就可以直接调试了。打印出来的输入最好能够直接 copy/paste 到新的测试用例里,如果格式不对,对于那么多行的输入,你需要一行一行调格式就太累了,而且这未必就只有一个失败的 case。fwy站长之家-易采站长站-Easck.Com

        所以我们把代码改成了下面这样:fwy站长之家-易采站长站-Easck.Com

        func FuzzSum(f *testing.F) {
          rand.Seed(time.Now().UnixNano())
          f.Add(10)
          f.Fuzz(func(t *testing.T, n int) {
            n %= 20
            var vals []int64
            var expect int64
            var buf strings.Builder
            buf.WriteString("\n")
            for i := 0; i < n; i++ {
              val := rand.Int63() % 1e6
              vals = append(vals, val)
              expect += val
              buf.WriteString(fmt.Sprintf("%d,\n", val))
            }
            assert.Equal(t, expect, Sum(vals), buf.String())
          })
        }
        

        再跑命令,得到如下结果:fwy站长之家-易采站长站-Easck.Com

        $ go test -fuzz=Sum
        fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed
        fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers
        fuzz: elapsed: 0s, execs: 1402 (10028/sec), new interesting: 10 (total: 8)
        --- FAIL: FuzzSum (0.16s)
            --- FAIL: FuzzSum (0.00s)
                sum_fuzz_test.go:34:
                      Error Trace:  sum_fuzz_test.go:34
                                          value.go:556
                                          value.go:339
                                          fuzz.go:334
                      Error:        Not equal:
                                    expected: 5823336
                                    actual  : 5623336
                      Test:         FuzzSum
                      Messages:
                                    799023,
                                    110387,
                                    811082,
                                    115543,
                                    859422,
                                    997646,
                                    200000,
                                    399008,
                                    7905,
                                    931332,
                                    591988,
            Failing input written to testdata/fuzz/FuzzSum/26d024acf85aae88f3291bf7e1c6f473eab8b051f2adb1bf05d4491bc49f5767
            To re-run:
            go test -run=FuzzSum/26d024acf85aae88f3291bf7e1c6f473eab8b051f2adb1bf05d4491bc49f5767
        FAIL
        exit status 1
        FAIL  github.com/kevwan/fuzzing  0.602s
        

        fwy站长之家-易采站长站-Easck.Com

        4.>

        根据上面的失败 case 的输出,我们可以 copy/paste 生成如下代码,当然框架是自己写的,输入参数可以直接拷贝进去。fwy站长之家-易采站长站-Easck.Com

        func TestSumFuzzCase1(t *testing.T) {
          vals := []int64{
            799023,
            110387,
            811082,
            115543,
            859422,
            997646,
            200000,
            399008,
            7905,
            931332,
            591988,
          }
          assert.Equal(t, int64(5823336), Sum(vals))
        }
        

        这样我们就可以很方便的调试了,并且能够增加一个有效 unit test,确保这个 bug 再也不会出现了。fwy站长之家-易采站长站-Easck.Com

        fwy站长之家-易采站长站-Easck.Com

        go>

        fwy站长之家-易采站长站-Easck.Com

        Go>

        我相信,Go 1.18 发布了,大多数项目线上代码不会立马升级到 1.18 的,那么 go fuzzing 引入的 testing.F 不能使用怎么办?fwy站长之家-易采站长站-Easck.Com

        线上(go.mod)不升级到 Go 1.18,但是我们本机是完全推荐升级的,那么这时我们只需要把上面的 FuzzSum 放到一个文件名类似 sum_fuzz_test.go 的文件里,然后在文件头加上如下指令即可:fwy站长之家-易采站长站-Easck.Com

        // go:build go1.18
        // +build go1.18
        

        注意:第三行必须是一个空行,否则就会变成 package 的注释了。fwy站长之家-易采站长站-Easck.Com

        这样我们在线上不管用哪个版本就不会报错了,而我们跑 fuzz testing 一般都是本机跑的,不受影响。fwy站长之家-易采站长站-Easck.Com

        fwy站长之家-易采站长站-Easck.Com

        go>

        上面讲的步骤是针对简单情况的,但有时根据失败 case 得到的输入形成新的 unit test 并不能复现问题时(特别是有 goroutine 死锁问题),问题就变得复杂起来了,如下输出你感受一下:fwy站长之家-易采站长站-Easck.Com

        go test -fuzz=MapReduce
        fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed
        fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers
        fuzz: elapsed: 3s, execs: 3681 (1227/sec), new interesting: 54 (total: 55)
        ...
        fuzz: elapsed: 1m21s, execs: 92705 (1101/sec), new interesting: 85 (total: 86)
        --- FAIL: FuzzMapReduce (80.96s)
            fuzzing process hung or terminated unexpectedly: exit status 2
            Failing input written to testdata/fuzz/FuzzMapReduce/ee6a61e8c968adad2e629fba11984532cac5d177c4899d3e0b7c2949a0a3d840
            To re-run:
            go test -run=FuzzMapReduce/ee6a61e8c968adad2e629fba11984532cac5d177c4899d3e0b7c2949a0a3d840
        FAIL
        exit status 1
        FAIL  github.com/zeromicro/go-zero/core/mr  81.471s
        

        这种情况下,只是告诉我们 fuzzing process 卡住了或者不正常结束了,状态码是2。这种情况下,一般 re-run 是不会复现的。为什么只是简单的返回错误码2呢?我仔细去看了 go fuzzing 的源码,每个 fuzzing test 都是一个单独的进程跑的,然后 go fuzzing 把模糊测试的进程输出扔掉了,只是显示了状态码。那么我们如何解决这个问题呢?fwy站长之家-易采站长站-Easck.Com

        我仔细分析了之后,决定自己来写一个类似 fuzzing test 的常规单元测试代码,这样就可以保证失败是在同一个进程内,并且会把错误信息打印到标准输出,代码大致如下:fwy站长之家-易采站长站-Easck.Com

        func TestSumFuzzRandom(t *testing.T) {
          const times = 100000
          rand.Seed(time.Now().UnixNano())
          for i := 0; i < times; i++ {
            n := rand.Intn(20)
            var vals []int64
            var expect int64
            var buf strings.Builder
            buf.WriteString("\n")
            for i := 0; i < n; i++ {
              val := rand.Int63() % 1e6
              vals = append(vals, val)
              expect += val
              buf.WriteString(fmt.Sprintf("%d,\n", val))
            }
            assert.Equal(t, expect, Sum(vals), buf.String())
          }
        }
        

        这样我们就可以自己来简单模拟一下 go fuzzing,但是任何错误我们可以得到清晰的输出。这里或许我没研究透 go fuzzing,或者还有其它方法可以控制,如果你知道,感谢告诉我一声。fwy站长之家-易采站长站-Easck.Com

        但这种需要跑很长时间的模拟 case,我们不会希望它在 CI 时每次都被执行,所以我把它放在一个单独的文件里,文件名类似 sum_fuzzcase_test.go,并在文件头加上了如下指令:fwy站长之家-易采站长站-Easck.Com

        // go:build fuzz
        // +build fuzz
        

        这样我们需要跑这个模拟 case 的时候加上 -tags fuzz 即可,比如:fwy站长之家-易采站长站-Easck.Com

        go test -tags fuzz ./...
        

        fwy站长之家-易采站长站-Easck.Com

        复杂用法示例

        上面介绍的是一个示例,还是比较简单的,如果遇到复杂场景不知道怎么写,可以先看看>

        MapReduce - github.com/zeromicro/g…fwy站长之家-易采站长站-Easck.Com

        模糊测试了 死锁 和 goroutine leak,特别是 chan + goroutine 的复杂场景可以借鉴fwy站长之家-易采站长站-Easck.Com

        stringx - github.com/zeromicro/g…fwy站长之家-易采站长站-Easck.Com

        模糊测试了常规的算法实现,对于算法类场景可以借鉴fwy站长之家-易采站长站-Easck.Com

        项目地址 github.com/zeromicro/g…fwy站长之家-易采站长站-Easck.Com

        以上就是Go语言开发代码自测绝佳go fuzzing用法详解的详细内容,更多关于Go开发go fuzzing代码自测的资料请关注易采站长站其它相关文章!fwy站长之家-易采站长站-Easck.Com

        如有侵权,请联系QQ:279390809 电话:15144810328

相关文章

  • 使用Go基于WebSocket构建千万级视频直播弹幕系统的代码详解

    使用Go基于WebSocket构建千万级视频直播弹幕系统的代码详解

    (1)业务复杂度介绍 开门见山,假设一个直播间同时500W人在线,那么1秒钟1000条弹幕,那么弹幕系统的推送频率就是: 500W * 1000条/秒=50亿条/秒 ,想想B站2019跨年晚会那次弹幕系统得是
    2020-07-08
  • golang中import cycle not allowed解决的一种思路

    golang中import cycle not allowed解决的一种思路

    发现问题 项目中碰到了一些问题,使用了指针函数的思路来解决相应问题 在实际项目中,因为两个项目互相引了对方的一些方法,导致了循环引用的错误,原本可以使用http的请求来解
    2019-11-10
  • 从go语言中找&和*区别详解

    从go语言中找&和*区别详解

    *和的区别 : 是取地址符号 , 即取得某个变量的地址 , 如 ; a*是指针运算符 , 可以表示一个变量是指针类型 , 也可以表示一个指针变量所指向的存储单元 , 也就是这个地址所存储的值 . 从
    2020-06-23
  • Go语言中利用http发起Get和Post请求的方法示例

    Go语言中利用http发起Get和Post请求的方法示例

    关于 HTTP 协议 HTTP(即超文本传输协议)是现代网络中最常见和常用的协议之一,设计它的目的是保证客户机和服务器之间的通信。 HTTP 的工作方式是客户机与服务器之间的 “请求-应答
    2019-11-10
  • golang如何实现mapreduce单进程版本详解

    golang如何实现mapreduce单进程版本详解

    前言 MapReduce作为hadoop的编程框架,是工程师最常接触的部分,也是除去了网络环境和集群配 置之外对整个Job执行效率影响很大的部分,所以很有必要深入了解整个过程。元旦放假的第一天
    2019-11-10
  • Go打包二进制文件的实现

    Go打包二进制文件的实现

    背景 众所周知,go语言可打包成目标平台二进制文件是其一大优势,如此go项目在服务器不需要配置go环境和依赖就可跑起来。 操作 需求:打包部署到centos7 笔者打包环境:mac os 方法:
    2020-03-11
  • GO语言实现简单的目录复制功能

    GO语言实现简单的目录复制功能

    本文实例讲述了GO语言实现简单的目录复制功能。分享给大家供大家参考。具体实现方法如下: 创建一个独立的 goroutine 遍历文件,主进程负责写入数据。程序会复制空目录,也可以设
    2019-11-10
  • golang中定时器cpu使用率高的现象详析

    golang中定时器cpu使用率高的现象详析

    前言: 废话少说,上线一个用golang写的高频的任务派发系统,上线跑着很稳定,但有个缺点就是当没有任务的时候,cpu的消耗也在几个百分点。 平均值在3%左右的cpu使用率。你没有任务
    2019-11-10