1
0
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:
Jonathan Amsterdam 2023-09-19 17:03:45 -04:00
parent 2b462646ed
commit 4d700a719b
3 changed files with 100 additions and 158 deletions

View File

@ -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)
}
}
} }

View File

@ -1,2 +0,0 @@
go test fuzz v1
[]byte("101$")

View File

@ -1,2 +0,0 @@
go test fuzz v1
[]byte("1010")