将模糊测试应用于解析某些字符串的函数

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 corpusF.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/.

的适当子目录中生成语料库

这应该足以让您入门。正如我在评论中所说,该功能处于开发初期,因此可能存在错误并且可能缺少文档。随着更多信息的出现,我可能会更新这个答案。