将模糊测试应用于解析某些字符串的函数
Apply fuzzing to a function that parses some string
最近 Go 团队发布了一个模糊器 https://blog.golang.org/fuzz-beta
你能帮我描述一下我对模糊器在测试目标方面的期望吗?
如何应用fuzzer?
在认为它足够好之前,请提供一些关于我们运行它需要多长时间的见解
如何将执行失败与代码相关联(我希望得到 GB 的结果,我想知道这会有多难以承受以及如何处理)
看到这段特意设计的很棒的代码,肯定需要对其进行模糊测试
package main
import (
"fmt"
"log"
)
func main() {
type expectation struct {
input string
output []string
}
expectations := []expectation{
expectation{
input: "foo=bar baz baz foo:1 baz ",
output: []string{
"foo=bar baz baz",
"foo:1 baz",
},
},
expectation{
input: "foo=bar baz baz foo:1 baz foo:234.mds32",
output: []string{
"foo=bar baz baz",
"foo:1 baz",
"foo:234.mds32",
},
},
expectation{
input: "foo=bar baz baz foo:1 baz foo:234.mds32 notfoo:baz foo:bak foo=bar baz foo:nospace foo:bar",
output: []string{
"foo=bar baz baz",
"foo:1 baz",
"foo:234.mds32",
"notfoo:baz",
"foo:bak",
"foo=bar baz",
"foo:nospace",
"foo:bar",
},
},
expectation{
input: "foo=bar",
output: []string{
"foo=bar",
},
},
expectation{
input: "foo",
output: []string{
"foo",
},
},
expectation{
input: "=bar",
output: []string{
"=bar",
},
},
expectation{
input: "foo=bar baz baz foo:::1 baz ",
output: []string{
"foo=bar baz baz",
"foo:::1 baz",
},
},
expectation{
input: "foo=bar baz baz foo:::1 baz ",
output: []string{
"foo=bar baz baz",
"foo:::1 baz",
},
},
}
for i, expectation := range expectations {
fmt.Println(" ==== TEST ", i)
success := true
res := parse(expectation.input)
if len(res) != len(expectation.output) {
log.Printf("invalid length of results for test %v\nwanted %#v\ngot %#v", i, expectation.output, res)
success = false
}
for e, r := range res {
if expectation.output[e] != r {
log.Printf("invalid result for test %v at index %v\nwanted %#v\ngot %#v", i, e, expectation.output, res)
success = false
}
}
if success {
fmt.Println(" ==== SUCCESS")
} else {
fmt.Println(" ==== FAILURE")
break
}
fmt.Println()
}
}
func parse(input string) (kvs []string) {
var lastSpace int
var nextLastSpace int
var n int
var since int
for i, r := range input {
if r == ' ' {
nextLastSpace = i + 1
if i > 0 && input[i-1] == ' ' {
continue
}
lastSpace = i
} else if r == '=' || r == ':' {
if n == 0 {
n++
continue
}
n++
if since < lastSpace {
kvs = append(kvs, string(input[since:lastSpace]))
}
if lastSpace < nextLastSpace { // there was multiple in between spaces.
since = nextLastSpace
} else {
since = lastSpace + 1
}
}
}
if since < len(input) { // still one entry
var begin int
var end int
begin = since
end = len(input)
if lastSpace > since { // rm trailing spaces it ends with 'foo:whatever '
end = lastSpace
} else if since < nextLastSpace { // rm starting spaces it ends with ' foo:whatever'
begin = nextLastSpace
}
kvs = append(kvs, string(input[begin:end]))
}
return
}
因此,我深入研究了模糊设计草案。这里有一些见解。
首先,按照 blog post 中的建议,您必须 运行 Go 提示:
go get golang.org/dl/gotip@latest
gotip download
gotip
命令可作为“go 命令的直接替代”,不会扰乱您当前的安装。
预期
fuzzer 基本上生成了一些函数输入参数的变体语料库,运行用它们进行测试以发现错误。
您无需自己编写任意数量的测试用例,而是向引擎提供示例输入,然后引擎会对其进行变异并使用新参数自动调用您的函数。然后语料库将被缓存,因此它将作为回归测试的基础。
如何应用fuzzer?
博客 post 和 draft design and the documentation at tip
对此进行了相当不错的介绍
testing
包现在有一个新类型 testing.F
,您可以将其传递给 fuzz 目标 。与单元测试和基准测试一样,模糊测试目标名称必须以 Fuzz
前缀开头。所以签名看起来像:
func FuzzBlah(f *testing.F) {
// ...
}
fuzz 目标主体本质上使用 testing.F
API 来:
提供 seed corpus 和 F.Add
The seed corpus is the user-specified set of inputs to a fuzz target which will be run by default with go test. These should be composed of meaningful inputs to test the behavior of the package, as well as a set of regression inputs for any newly discovered bugs identified by the fuzzing engine
所以这些是您的 parse
函数的实际测试用例输入,您自己编写的那些。
func FuzzBlah(f *testing.F) {
f.Add("foo=bar")
f.Add("foo=bar baz baz foo:1 baz ")
// and so on
}
运行 带有 F.Fuzz
模糊输入的函数
每个模糊测试目标调用 f.Fuzz
一次。 Fuzz
的参数是一个函数,它接受一个 testing.T
和 N 个与传递给 f.Add
的参数类型相同的参数。如果您的示例测试只需要一个字符串,它将是:
func FuzzBlah(f *testing.F) {
f.Add("foo=bar")
f.Add("foo=bar baz baz foo:1 baz ")
f.Fuzz(func(t *testing.T, input string) {
})
}
fuzz 函数的主体就是您想要测试的任何内容,例如您的 parse
函数。
我认为,理解和使用模糊器的关键在于您不测试成对的输入和预期输出。您可以通过单元测试来做到这一点。
通过模糊测试,您可以测试代码不会因给定输入而中断。给定的输入是随机的,足以覆盖极端情况。这就是为什么官方的例子:
- 在预期失败的情况下调用
t.Skip()
- 运行 countercheck 函数,例如
Marshal
然后 Unmarshal
,或者 url.ParseQuery
然后 query.Encode
输入编组正确但随后未解编回原始值的情况是意外失败,而那些模糊器比你更擅长发现的情况编写手动测试。
因此,将它们放在一起,模糊测试目标可以是:
func FuzzBlah(f *testing.F) {
f.Add("foo=bar")
f.Add("foo=bar baz baz foo:1 baz ")
// and so on
f.Fuzz(func(t *testing.T, input string) {
out := parse(input)
// bad output, skip
if len(out) == 0 {
t.Skip()
}
// countercheck
enc := encode(out)
if enc != input {
t.Errorf("countercheck failed")
}
})
}
导致测试失败的输入将被添加到语料库中,因此您可以修复代码和 运行 回归测试。
运行宁可
你只是 运行 go test
带有 -fuzz <regex>
标志。您可以指定模糊器 运行s 的持续时间或次数:
-fuzztime <duration>
其中持续时间是 time.Duration
string,例如10m
-fuzztime Nx
其中 N 是迭代次数,例如20x
您的测试需要多长时间或多少取决于您正在测试的代码。我相信 Go 团队会在适当的时候提供更多相关建议。
总结一下:
gotip test -fuzz . -fuzztime 20x
这也会在 $GOCACHE/fuzz/
.
的适当子目录中生成语料库
这应该足以让您入门。正如我在评论中所说,该功能处于开发初期,因此可能存在错误并且可能缺少文档。随着更多信息的出现,我可能会更新这个答案。
最近 Go 团队发布了一个模糊器 https://blog.golang.org/fuzz-beta
你能帮我描述一下我对模糊器在测试目标方面的期望吗?
如何应用fuzzer?
在认为它足够好之前,请提供一些关于我们运行它需要多长时间的见解
如何将执行失败与代码相关联(我希望得到 GB 的结果,我想知道这会有多难以承受以及如何处理)
看到这段特意设计的很棒的代码,肯定需要对其进行模糊测试
package main
import (
"fmt"
"log"
)
func main() {
type expectation struct {
input string
output []string
}
expectations := []expectation{
expectation{
input: "foo=bar baz baz foo:1 baz ",
output: []string{
"foo=bar baz baz",
"foo:1 baz",
},
},
expectation{
input: "foo=bar baz baz foo:1 baz foo:234.mds32",
output: []string{
"foo=bar baz baz",
"foo:1 baz",
"foo:234.mds32",
},
},
expectation{
input: "foo=bar baz baz foo:1 baz foo:234.mds32 notfoo:baz foo:bak foo=bar baz foo:nospace foo:bar",
output: []string{
"foo=bar baz baz",
"foo:1 baz",
"foo:234.mds32",
"notfoo:baz",
"foo:bak",
"foo=bar baz",
"foo:nospace",
"foo:bar",
},
},
expectation{
input: "foo=bar",
output: []string{
"foo=bar",
},
},
expectation{
input: "foo",
output: []string{
"foo",
},
},
expectation{
input: "=bar",
output: []string{
"=bar",
},
},
expectation{
input: "foo=bar baz baz foo:::1 baz ",
output: []string{
"foo=bar baz baz",
"foo:::1 baz",
},
},
expectation{
input: "foo=bar baz baz foo:::1 baz ",
output: []string{
"foo=bar baz baz",
"foo:::1 baz",
},
},
}
for i, expectation := range expectations {
fmt.Println(" ==== TEST ", i)
success := true
res := parse(expectation.input)
if len(res) != len(expectation.output) {
log.Printf("invalid length of results for test %v\nwanted %#v\ngot %#v", i, expectation.output, res)
success = false
}
for e, r := range res {
if expectation.output[e] != r {
log.Printf("invalid result for test %v at index %v\nwanted %#v\ngot %#v", i, e, expectation.output, res)
success = false
}
}
if success {
fmt.Println(" ==== SUCCESS")
} else {
fmt.Println(" ==== FAILURE")
break
}
fmt.Println()
}
}
func parse(input string) (kvs []string) {
var lastSpace int
var nextLastSpace int
var n int
var since int
for i, r := range input {
if r == ' ' {
nextLastSpace = i + 1
if i > 0 && input[i-1] == ' ' {
continue
}
lastSpace = i
} else if r == '=' || r == ':' {
if n == 0 {
n++
continue
}
n++
if since < lastSpace {
kvs = append(kvs, string(input[since:lastSpace]))
}
if lastSpace < nextLastSpace { // there was multiple in between spaces.
since = nextLastSpace
} else {
since = lastSpace + 1
}
}
}
if since < len(input) { // still one entry
var begin int
var end int
begin = since
end = len(input)
if lastSpace > since { // rm trailing spaces it ends with 'foo:whatever '
end = lastSpace
} else if since < nextLastSpace { // rm starting spaces it ends with ' foo:whatever'
begin = nextLastSpace
}
kvs = append(kvs, string(input[begin:end]))
}
return
}
因此,我深入研究了模糊设计草案。这里有一些见解。
首先,按照 blog post 中的建议,您必须 运行 Go 提示:
go get golang.org/dl/gotip@latest
gotip download
gotip
命令可作为“go 命令的直接替代”,不会扰乱您当前的安装。
预期
fuzzer 基本上生成了一些函数输入参数的变体语料库,运行用它们进行测试以发现错误。
您无需自己编写任意数量的测试用例,而是向引擎提供示例输入,然后引擎会对其进行变异并使用新参数自动调用您的函数。然后语料库将被缓存,因此它将作为回归测试的基础。
如何应用fuzzer?
博客 post 和 draft design and the documentation at tip
对此进行了相当不错的介绍testing
包现在有一个新类型 testing.F
,您可以将其传递给 fuzz 目标 。与单元测试和基准测试一样,模糊测试目标名称必须以 Fuzz
前缀开头。所以签名看起来像:
func FuzzBlah(f *testing.F) {
// ...
}
fuzz 目标主体本质上使用 testing.F
API 来:
提供 seed corpus 和 F.Add
The seed corpus is the user-specified set of inputs to a fuzz target which will be run by default with go test. These should be composed of meaningful inputs to test the behavior of the package, as well as a set of regression inputs for any newly discovered bugs identified by the fuzzing engine
所以这些是您的 parse
函数的实际测试用例输入,您自己编写的那些。
func FuzzBlah(f *testing.F) {
f.Add("foo=bar")
f.Add("foo=bar baz baz foo:1 baz ")
// and so on
}
运行 带有 F.Fuzz
每个模糊测试目标调用 f.Fuzz
一次。 Fuzz
的参数是一个函数,它接受一个 testing.T
和 N 个与传递给 f.Add
的参数类型相同的参数。如果您的示例测试只需要一个字符串,它将是:
func FuzzBlah(f *testing.F) {
f.Add("foo=bar")
f.Add("foo=bar baz baz foo:1 baz ")
f.Fuzz(func(t *testing.T, input string) {
})
}
fuzz 函数的主体就是您想要测试的任何内容,例如您的 parse
函数。
我认为,理解和使用模糊器的关键在于您不测试成对的输入和预期输出。您可以通过单元测试来做到这一点。
通过模糊测试,您可以测试代码不会因给定输入而中断。给定的输入是随机的,足以覆盖极端情况。这就是为什么官方的例子:
- 在预期失败的情况下调用
t.Skip()
- 运行 countercheck 函数,例如
Marshal
然后Unmarshal
,或者url.ParseQuery
然后query.Encode
输入编组正确但随后未解编回原始值的情况是意外失败,而那些模糊器比你更擅长发现的情况编写手动测试。
因此,将它们放在一起,模糊测试目标可以是:
func FuzzBlah(f *testing.F) {
f.Add("foo=bar")
f.Add("foo=bar baz baz foo:1 baz ")
// and so on
f.Fuzz(func(t *testing.T, input string) {
out := parse(input)
// bad output, skip
if len(out) == 0 {
t.Skip()
}
// countercheck
enc := encode(out)
if enc != input {
t.Errorf("countercheck failed")
}
})
}
导致测试失败的输入将被添加到语料库中,因此您可以修复代码和 运行 回归测试。
运行宁可
你只是 运行 go test
带有 -fuzz <regex>
标志。您可以指定模糊器 运行s 的持续时间或次数:
-fuzztime <duration>
其中持续时间是time.Duration
string,例如10m
-fuzztime Nx
其中 N 是迭代次数,例如20x
您的测试需要多长时间或多少取决于您正在测试的代码。我相信 Go 团队会在适当的时候提供更多相关建议。
总结一下:
gotip test -fuzz . -fuzztime 20x
这也会在 $GOCACHE/fuzz/
.
这应该足以让您入门。正如我在评论中所说,该功能处于开发初期,因此可能存在错误并且可能缺少文档。随着更多信息的出现,我可能会更新这个答案。