前言#
我们昨天简单的了解了下泛型的用法
https://zhuanlan.zhihu.com/p/620818505
今天我们来了解下fuzzing也就是模糊测试
fuzzing#
项目初始化#
mkdir fuzz
cd fuzz
go mod init example/fuzz
echo > main.go // 编码可能是utf-16,会导致编译失败功能#
我们来写一个反转字符串的算法
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev := Reverse(input)
doubleRev := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q\n", rev)
fmt.Printf("reversed again: %q\n", doubleRev)
}代码挺好理解,没啥好说的。
不过需要注意下这里是基于bytes处理的,后面会遇到问题。

单元测试#
然后我们来写一个单元测试
不过需要先创建一个reverse_test.go文件
package main
import "testing"
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}
模糊测试#
在实现之前,我们需要明晰为啥要用模糊测试。对于单元测试来说,最大的问题就是当一块功能场景多起来之后就可能存在遗漏的测试点比如边缘问题,并且所有测试点都需要我们去手动写上期望值。
而模糊测试就是为了方便这一点的,不需要开发者们去手动写上期望值以及输入值。
注意:我们可以把unit test、benchmarks以及fuzz test写在同一个test文件中。
我们来把前面的TestReverse方法改为FuzzReverse
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
} f.Add将对应的文本添加到语料库中,这个是测试用的源数据,到时候会被传递给orig这个参数。
前面说的很玄乎,实际上就是一个回调用来定义期望逻辑,然后之后专注于输入即可。
和unit test相比,这里确实变得方便了较多,省去了expect的逻辑。
不过这里有些取巧了,通过反转两次来判断是否和之前的一样来达到判断是否符合预期的效果,我们用unit test实际上也没啥问题。
这里你应该注意到了,对于模糊测试,我们并不能预测到输出的内容。
所以这个时候就需要我们发散思维去实现传给f.Fuzz的回调,这就是和单元测试最大的不同:灵活。
然后我们来运行下go test .
你也可以运行go test -run=FuzzReserve指定某个测试元。

测试没通过
为什么呢?因为我偷偷的测试了另一个指令go test -fuzz=Fuzz
它会在当前文件夹下生成一个testdata\fuzz\函数名的目录,然后你的测试结果都在这里面。

这个类似乱码的东西则表示我们转换的过程失败了,v1表示encode的版本,从第一行往下开始表示每个输入对应的输出。
当我们把这个文件夹移除后再重新go test .就会发现正常了(如果不删除这个文件夹而是运行时去掉-fuzz依旧是会失败的,因为它已经被标记到语料库里了)

-fuzz=Fuzz这个命令行参数表示开启模糊测试,会查看是否有任何随机生成的字符串输入会导致失败。
修复#
那么为什么会失败呢?看前面的错误描述“不是有效的utf-8编码的字符”来自于这段代码
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}那就可以推测这个反转的过程中字符的编码变化了,进一步分析出这是因为我们是byte-by-byte。我们前面说过string是一个集合(好像不是go里的?不过没差,问题都是因为字符串里面字符占用多少个字节的问题,如果不对麻烦评论区说下,谢谢~)。比如汉字字符占位两个字节,如果通过byte交换的方式就会将字符“切割”。
补:go中的字符串是一段只读的bytes切片[1]
所以我们需要将byte-by-byte替换为tune-by-tune,每次交换的时候都是交换字符。
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
} 然后我们go test试下

正常,然后我们再来-fuzz试下。

又没通过,不过这次是另一个bug。
简单看下描述,发现原本的符号就是不符合utf-8的
那么这个就简单了,咱直接在入口那里判断是否是符合utf-8的,如果不是直接return
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev, revErr := Reverse(input)
doubleRev, doubleRevErr := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}
go run .
正常,然后改下FuzzReserve方法
package main
import (
"testing"
"unicode/utf8"
)
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
} 处理了下错误场景,重新运行下应该没什么问题了go test -fuzz=Fuzz

如何没有问题,那么就会一直持续下去直到遇到错误。
这个时候我们也可以使用另一个参数-fuzzTime用来指定跑多少秒即可。
go test -fuzz=Fuzz -fuzzTime=30如果没有问题,最多只会持续30秒。
如果需要了解更多fuzz,可以看这篇文章:https://go.dev/security/fuzz/#command-line-output
总结#
~
参考#
发布于 2023-04-12 16:38・IP 属地广东
