mirror of
https://github.com/golang/go
synced 2024-11-18 21:05:02 -07:00
net/http: test index exhaustively
Replace the fuzz test with one that enumerates all relevant patterns up to a certain length. For conflict detection, we don't need to check every possible method, host and segment, only a few that cover all the possibilities. There are only 2400 distinct patterns in the corpus we generate, and the test generates, indexes and compares them all in about a quarter of a second. Change-Id: I9fde88e87cec07b1b244306119e4e71f7205bb77 Reviewed-on: https://go-review.googlesource.com/c/go/+/529556 Run-TryBot: Jonathan Amsterdam <jba@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Damien Neil <dneil@google.com>
This commit is contained in:
parent
2b462646ed
commit
4d700a719b
@ -5,7 +5,6 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
@ -14,66 +13,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestIndex(t *testing.T) {
|
func TestIndex(t *testing.T) {
|
||||||
pats := []string{"HEAD /", "/a"}
|
// Generate every kind of pattern up to some number of segments,
|
||||||
|
// and compare conflicts found during indexing with those found
|
||||||
var patterns []*pattern
|
// by exhaustive comparison.
|
||||||
|
patterns := generatePatterns()
|
||||||
var idx routingIndex
|
var idx routingIndex
|
||||||
for _, p := range pats {
|
for i, pat := range patterns {
|
||||||
pat := mustParsePattern(t, p)
|
got := indexConflicts(pat, &idx)
|
||||||
patterns = append(patterns, pat)
|
want := trueConflicts(pat, patterns[:i])
|
||||||
|
if !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("%q:\ngot %q\nwant %q", pat, got, want)
|
||||||
|
}
|
||||||
idx.addPattern(pat)
|
idx.addPattern(pat)
|
||||||
}
|
}
|
||||||
|
|
||||||
compare := func(pat *pattern) {
|
|
||||||
t.Helper()
|
|
||||||
got := indexConflicts(pat, &idx)
|
|
||||||
want := trueConflicts(pat, patterns)
|
|
||||||
if !slices.Equal(got, want) {
|
|
||||||
t.Errorf("%q:\ngot %q\nwant %q", pat, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compare(mustParsePattern(t, "GET /foo"))
|
|
||||||
compare(mustParsePattern(t, "GET /{x}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test works by comparing possiblyConflictingPatterns with
|
|
||||||
// an exhaustive loop through all patterns.
|
|
||||||
func FuzzIndex(f *testing.F) {
|
|
||||||
inits := []string{"/a", "/a/b", "/{x0}", "/{x0}/b", "/a/{x0}", "/a/{$}", "/a/b/{$}",
|
|
||||||
"/a/", "/a/b/", "/{x}/b/c/{$}", "GET /{x0}/", "HEAD /a"}
|
|
||||||
|
|
||||||
var patterns []*pattern
|
|
||||||
var idx routingIndex
|
|
||||||
|
|
||||||
// compare takes a fatalf function because fuzzing doesn't like
|
|
||||||
// it when the fuzz function calls f.Fatalf.
|
|
||||||
compare := func(pat *pattern, fatalf func(string, ...any)) {
|
|
||||||
got := indexConflicts(pat, &idx)
|
|
||||||
want := trueConflicts(pat, patterns)
|
|
||||||
if !slices.Equal(got, want) {
|
|
||||||
fatalf("%q:\ngot %q\nwant %q", pat, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range inits {
|
|
||||||
pat, err := parsePattern(p)
|
|
||||||
if err != nil {
|
|
||||||
f.Fatal(err)
|
|
||||||
}
|
|
||||||
compare(pat, f.Fatalf)
|
|
||||||
patterns = append(patterns, pat)
|
|
||||||
idx.addPattern(pat)
|
|
||||||
f.Add(bytesFromPattern(pat))
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, pb []byte) {
|
|
||||||
pat := bytesToPattern(pb)
|
|
||||||
if pat == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
compare(pat, t.Fatalf)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func trueConflicts(pat *pattern, pats []*pattern) []string {
|
func trueConflicts(pat *pattern, pats []*pattern) []string {
|
||||||
@ -99,109 +51,103 @@ func indexConflicts(pat *pattern, idx *routingIndex) []string {
|
|||||||
return slices.Compact(s)
|
return slices.Compact(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: incorporate host and method; make encoding denser.
|
// generatePatterns generates all possible patterns using a representative
|
||||||
func bytesToPattern(bs []byte) *pattern {
|
// sample of parts.
|
||||||
if len(bs) == 0 {
|
func generatePatterns() []*pattern {
|
||||||
return nil
|
var pats []*pattern
|
||||||
}
|
|
||||||
var sb strings.Builder
|
collect := func(s string) {
|
||||||
|
// Replace duplicate wildcards with unique ones.
|
||||||
|
var b strings.Builder
|
||||||
wc := 0
|
wc := 0
|
||||||
for _, b := range bs[:len(bs)-1] {
|
for {
|
||||||
sb.WriteByte('/')
|
i := strings.Index(s, "{x}")
|
||||||
switch b & 0x3 {
|
if i < 0 {
|
||||||
case 0:
|
b.WriteString(s)
|
||||||
fmt.Fprintf(&sb, "{x%d}", wc)
|
break
|
||||||
|
}
|
||||||
|
b.WriteString(s[:i])
|
||||||
|
fmt.Fprintf(&b, "{x%d}", wc)
|
||||||
wc++
|
wc++
|
||||||
case 1:
|
s = s[i+3:]
|
||||||
sb.WriteString("a")
|
|
||||||
case 2:
|
|
||||||
sb.WriteString("b")
|
|
||||||
case 3:
|
|
||||||
sb.WriteString("c")
|
|
||||||
}
|
}
|
||||||
}
|
pat, err := parsePattern(b.String())
|
||||||
sb.WriteByte('/')
|
|
||||||
switch bs[len(bs)-1] & 0x7 {
|
|
||||||
case 0:
|
|
||||||
fmt.Fprintf(&sb, "{x%d}", wc)
|
|
||||||
case 1:
|
|
||||||
sb.WriteString("a")
|
|
||||||
case 2:
|
|
||||||
sb.WriteString("b")
|
|
||||||
case 3:
|
|
||||||
sb.WriteString("c")
|
|
||||||
case 4, 5:
|
|
||||||
fmt.Fprintf(&sb, "{x%d...}", wc)
|
|
||||||
default:
|
|
||||||
sb.WriteString("{$}")
|
|
||||||
}
|
|
||||||
pat, err := parsePattern(sb.String())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return pat
|
pats = append(pats, pat)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
methods = []string{"", "GET ", "HEAD ", "POST "}
|
||||||
|
hosts = []string{"", "h1", "h2"}
|
||||||
|
segs = []string{"/a", "/b", "/{x}"}
|
||||||
|
finalSegs = []string{"/a", "/b", "/{f}", "/{m...}", "/{$}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
g := genConcat(
|
||||||
|
genChoice(methods),
|
||||||
|
genChoice(hosts),
|
||||||
|
genStar(3, genChoice(segs)),
|
||||||
|
genChoice(finalSegs))
|
||||||
|
g(collect)
|
||||||
|
return pats
|
||||||
}
|
}
|
||||||
|
|
||||||
func bytesFromPattern(p *pattern) []byte {
|
// A generator is a function that calls its argument with the strings that it
|
||||||
var bs []byte
|
// generates.
|
||||||
for _, s := range p.segments {
|
type generator func(collect func(string))
|
||||||
var b byte
|
|
||||||
switch {
|
// genConst generates a single constant string.
|
||||||
case s.multi:
|
func genConst(s string) generator {
|
||||||
b = 4
|
return func(collect func(string)) {
|
||||||
case s.wild:
|
collect(s)
|
||||||
b = 0
|
|
||||||
case s.s == "/":
|
|
||||||
b = 7
|
|
||||||
case s.s == "a":
|
|
||||||
b = 1
|
|
||||||
case s.s == "b":
|
|
||||||
b = 2
|
|
||||||
case s.s == "c":
|
|
||||||
b = 3
|
|
||||||
default:
|
|
||||||
panic("bad pattern")
|
|
||||||
}
|
}
|
||||||
bs = append(bs, b)
|
|
||||||
}
|
|
||||||
return bs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBytesPattern(t *testing.T) {
|
// genChoice generates all the strings in its argument.
|
||||||
tests := []struct {
|
func genChoice(choices []string) generator {
|
||||||
bs []byte
|
return func(collect func(string)) {
|
||||||
pat string
|
for _, c := range choices {
|
||||||
}{
|
collect(c)
|
||||||
{[]byte{0, 1, 2, 3}, "/{x0}/a/b/c"},
|
|
||||||
{[]byte{16, 17, 18, 19}, "/{x0}/a/b/c"},
|
|
||||||
{[]byte{4, 4}, "/{x0}/{x1...}"},
|
|
||||||
{[]byte{6, 7}, "/b/{$}"},
|
|
||||||
}
|
|
||||||
t.Run("To", func(t *testing.T) {
|
|
||||||
for _, test := range tests {
|
|
||||||
p := bytesToPattern(test.bs)
|
|
||||||
got := p.String()
|
|
||||||
if got != test.pat {
|
|
||||||
t.Errorf("%v: got %q, want %q", test.bs, got, test.pat)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// genConcat2 generates the cross product of the strings of g1 concatenated
|
||||||
|
// with those of g2.
|
||||||
|
func genConcat2(g1, g2 generator) generator {
|
||||||
|
return func(collect func(string)) {
|
||||||
|
g1(func(s1 string) {
|
||||||
|
g2(func(s2 string) {
|
||||||
|
collect(s1 + s2)
|
||||||
})
|
})
|
||||||
t.Run("From", func(t *testing.T) {
|
|
||||||
for _, test := range tests {
|
|
||||||
p, err := parsePattern(test.pat)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
got := bytesFromPattern(p)
|
|
||||||
var want []byte
|
|
||||||
for _, b := range test.bs[:len(test.bs)-1] {
|
|
||||||
want = append(want, b%4)
|
|
||||||
|
|
||||||
}
|
|
||||||
want = append(want, test.bs[len(test.bs)-1]%8)
|
|
||||||
if !bytes.Equal(got, want) {
|
|
||||||
t.Errorf("%s: got %v, want %v", test.pat, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// genConcat generalizes genConcat2 to any number of generators.
|
||||||
|
func genConcat(gs ...generator) generator {
|
||||||
|
if len(gs) == 0 {
|
||||||
|
return genConst("")
|
||||||
|
}
|
||||||
|
return genConcat2(gs[0], genConcat(gs[1:]...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// genRepeat generates strings of exactly n copies of g's strings.
|
||||||
|
func genRepeat(n int, g generator) generator {
|
||||||
|
if n == 0 {
|
||||||
|
return genConst("")
|
||||||
|
}
|
||||||
|
return genConcat(g, genRepeat(n-1, g))
|
||||||
|
}
|
||||||
|
|
||||||
|
// genStar (named after the Kleene star) generates 0, 1, 2, ..., max
|
||||||
|
// copies of the strings of g.
|
||||||
|
func genStar(max int, g generator) generator {
|
||||||
|
return func(collect func(string)) {
|
||||||
|
for i := 0; i <= max; i++ {
|
||||||
|
genRepeat(i, g)(collect)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("101$")
|
|
@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("1010")
|
|
Loading…
Reference in New Issue
Block a user