testing: import new version of match.go

Этот коммит содержится в:
Damian Gryski 2023-03-31 12:54:03 -07:00 коммит произвёл Damian Gryski
родитель 50d681359d
коммит ee81c31884
4 изменённых файлов: 349 добавлений и 126 удалений

Просмотреть файл

@ -365,7 +365,7 @@ func runBenchmarks(matchString func(pat, str string) (bool, error), benchmarks [
return true return true
} }
ctx := &benchContext{ ctx := &benchContext{
match: newMatcher(matchString, *matchBenchmarks, "-test.bench"), match: newMatcher(matchString, *matchBenchmarks, "-test.bench", ""),
} }
var bs []InternalBenchmark var bs []InternalBenchmark
for _, Benchmark := range benchmarks { for _, Benchmark := range benchmarks {

Просмотреть файл

@ -14,41 +14,73 @@ import (
// matcher sanitizes, uniques, and filters names of subtests and subbenchmarks. // matcher sanitizes, uniques, and filters names of subtests and subbenchmarks.
type matcher struct { type matcher struct {
filter []string filter filterMatch
skip filterMatch
matchFunc func(pat, str string) (bool, error) matchFunc func(pat, str string) (bool, error)
mu sync.Mutex mu sync.Mutex
subNames map[string]int64
// subNames is used to deduplicate subtest names.
// Each key is the subtest name joined to the deduplicated name of the parent test.
// Each value is the count of the number of occurrences of the given subtest name
// already seen.
subNames map[string]int32
} }
type filterMatch interface {
// matches checks the name against the receiver's pattern strings using the
// given match function.
matches(name []string, matchString func(pat, str string) (bool, error)) (ok, partial bool)
// verify checks that the receiver's pattern strings are valid filters by
// calling the given match function.
verify(name string, matchString func(pat, str string) (bool, error)) error
}
// simpleMatch matches a test name if all of the pattern strings match in
// sequence.
type simpleMatch []string
// alternationMatch matches a test name if one of the alternations match.
type alternationMatch []filterMatch
// TODO: fix test_main to avoid race and improve caching, also allowing to // TODO: fix test_main to avoid race and improve caching, also allowing to
// eliminate this Mutex. // eliminate this Mutex.
var matchMutex sync.Mutex var matchMutex sync.Mutex
func newMatcher(matchString func(pat, str string) (bool, error), patterns, name string) *matcher { func allMatcher() *matcher {
return newMatcher(nil, "", "", "")
}
func newMatcher(matchString func(pat, str string) (bool, error), patterns, name, skips string) *matcher {
if isBaremetal { if isBaremetal {
// Probably not enough ram to load regexp, substitute something simpler.
matchString = fakeMatchString matchString = fakeMatchString
} }
var filter []string var filter, skip filterMatch
if patterns != "" { if patterns == "" {
filter = simpleMatch{} // always partial true
} else {
filter = splitRegexp(patterns) filter = splitRegexp(patterns)
for i, s := range filter { if err := filter.verify(name, matchString); err != nil {
filter[i] = rewrite(s) fmt.Fprintf(os.Stderr, "testing: invalid regexp for %s\n", err)
os.Exit(1)
} }
// Verify filters before doing any processing. }
for i, s := range filter { if skips == "" {
if _, err := matchString(s, "non-empty"); err != nil { skip = alternationMatch{} // always false
fmt.Fprintf(os.Stderr, "testing: invalid regexp for element %d of %s (%q): %s\n", i, name, s, err) } else {
os.Exit(1) skip = splitRegexp(skips)
} if err := skip.verify("-test.skip", matchString); err != nil {
fmt.Fprintf(os.Stderr, "testing: invalid regexp for %v\n", err)
os.Exit(1)
} }
} }
return &matcher{ return &matcher{
filter: filter, filter: filter,
skip: skip,
matchFunc: matchString, matchFunc: matchString,
subNames: map[string]int64{}, subNames: map[string]int32{},
} }
} }
@ -65,22 +97,83 @@ func (m *matcher) fullName(c *common, subname string) (name string, ok, partial
matchMutex.Lock() matchMutex.Lock()
defer matchMutex.Unlock() defer matchMutex.Unlock()
// We check the full array of paths each time to allow for the case that // We check the full array of paths each time to allow for the case that a pattern contains a '/'.
// a pattern contains a '/'.
elem := strings.Split(name, "/") elem := strings.Split(name, "/")
for i, s := range elem {
if i >= len(m.filter) { // filter must match.
break // accept partial match that may produce full match later.
} ok, partial = m.filter.matches(elem, m.matchFunc)
if ok, _ := m.matchFunc(m.filter[i], s); !ok { if !ok {
return name, false, false return name, false, false
}
} }
return name, true, len(elem) < len(m.filter)
// skip must not match.
// ignore partial match so we can get to more precise match later.
skip, partialSkip := m.skip.matches(elem, m.matchFunc)
if skip && !partialSkip {
return name, false, false
}
return name, ok, partial
} }
func splitRegexp(s string) []string { // clearSubNames clears the matcher's internal state, potentially freeing
a := make([]string, 0, strings.Count(s, "/")) // memory. After this is called, T.Name may return the same strings as it did
// for earlier subtests.
func (m *matcher) clearSubNames() {
m.mu.Lock()
defer m.mu.Unlock()
for key := range m.subNames {
delete(m.subNames, key)
}
}
func (m simpleMatch) matches(name []string, matchString func(pat, str string) (bool, error)) (ok, partial bool) {
for i, s := range name {
if i >= len(m) {
break
}
if ok, _ := matchString(m[i], s); !ok {
return false, false
}
}
return true, len(name) < len(m)
}
func (m simpleMatch) verify(name string, matchString func(pat, str string) (bool, error)) error {
for i, s := range m {
m[i] = rewrite(s)
}
// Verify filters before doing any processing.
for i, s := range m {
if _, err := matchString(s, "non-empty"); err != nil {
return fmt.Errorf("element %d of %s (%q): %s", i, name, s, err)
}
}
return nil
}
func (m alternationMatch) matches(name []string, matchString func(pat, str string) (bool, error)) (ok, partial bool) {
for _, m := range m {
if ok, partial = m.matches(name, matchString); ok {
return ok, partial
}
}
return false, false
}
func (m alternationMatch) verify(name string, matchString func(pat, str string) (bool, error)) error {
for i, m := range m {
if err := m.verify(name, matchString); err != nil {
return fmt.Errorf("alternation %d of %s", i, err)
}
}
return nil
}
func splitRegexp(s string) filterMatch {
a := make(simpleMatch, 0, strings.Count(s, "/"))
b := make(alternationMatch, 0, strings.Count(s, "|"))
cs := 0 cs := 0
cp := 0 cp := 0
for i := 0; i < len(s); { for i := 0; i < len(s); {
@ -108,33 +201,90 @@ func splitRegexp(s string) []string {
i = 0 i = 0
continue continue
} }
case '|':
if cs == 0 && cp == 0 {
a = append(a, s[:i])
s = s[i+1:]
i = 0
b = append(b, a)
a = make(simpleMatch, 0, len(a))
continue
}
} }
i++ i++
} }
return append(a, s)
a = append(a, s)
if len(b) == 0 {
return a
}
return append(b, a)
} }
// unique creates a unique name for the given parent and subname by affixing it // unique creates a unique name for the given parent and subname by affixing it
// with one or more counts, if necessary. // with one or more counts, if necessary.
func (m *matcher) unique(parent, subname string) string { func (m *matcher) unique(parent, subname string) string {
name := fmt.Sprintf("%s/%s", parent, subname) base := parent + "/" + subname
empty := subname == ""
for {
next, exists := m.subNames[name]
if !empty && !exists {
m.subNames[name] = 1 // next count is 1
return name
}
// Name was already used. We increment with the count and append a
// string with the count.
m.subNames[name] = next + 1
// Add a count to guarantee uniqueness. for {
name = fmt.Sprintf("%s#%02d", name, next) n := m.subNames[base]
empty = false if n < 0 {
panic("subtest count overflow")
}
m.subNames[base] = n + 1
if n == 0 && subname != "" {
prefix, nn := parseSubtestNumber(base)
if len(prefix) < len(base) && nn < m.subNames[prefix] {
// This test is explicitly named like "parent/subname#NN",
// and #NN was already used for the NNth occurrence of "parent/subname".
// Loop to add a disambiguating suffix.
continue
}
return base
}
name := fmt.Sprintf("%s#%02d", base, n)
if m.subNames[name] != 0 {
// This is the nth occurrence of base, but the name "parent/subname#NN"
// collides with the first occurrence of a subtest *explicitly* named
// "parent/subname#NN". Try the next number.
continue
}
return name
} }
} }
// parseSubtestNumber splits a subtest name into a "#%02d"-formatted int32
// suffix (if present), and a prefix preceding that suffix (always).
func parseSubtestNumber(s string) (prefix string, nn int32) {
i := strings.LastIndex(s, "#")
if i < 0 {
return s, 0
}
prefix, suffix := s[:i], s[i+1:]
if len(suffix) < 2 || (len(suffix) > 2 && suffix[0] == '0') {
// Even if suffix is numeric, it is not a possible output of a "%02" format
// string: it has either too few digits or too many leading zeroes.
return s, 0
}
if suffix == "00" {
if !strings.HasSuffix(prefix, "/") {
// We only use "#00" as a suffix for subtests named with the empty
// string — it isn't a valid suffix if the subtest name is non-empty.
return s, 0
}
}
n, err := strconv.ParseInt(suffix, 10, 32)
if err != nil || n < 0 {
return s, 0
}
return prefix, int32(n)
}
// rewrite rewrites a subname to having only printable characters and no white // rewrite rewrites a subname to having only printable characters and no white
// space. // space.
func rewrite(s string) string { func rewrite(s string) string {
@ -171,14 +321,3 @@ func isSpace(r rune) bool {
} }
return false return false
} }
// A fake regexp matcher.
// Inflexible, but saves 50KB of flash and 50KB of RAM per -size full,
// and lets tests pass on cortex-m.
func fakeMatchString(pat, str string) (bool, error) {
if pat == ".*" {
return true, nil
}
matched := strings.Contains(str, pat)
return matched, nil
}

Просмотреть файл

@ -5,8 +5,10 @@
package testing package testing
import ( import (
"fmt"
"reflect" "reflect"
"regexp" "regexp"
"strings"
"unicode" "unicode"
) )
@ -25,10 +27,11 @@ func TestIsSpace(t *T) {
} }
func TestSplitRegexp(t *T) { func TestSplitRegexp(t *T) {
res := func(s ...string) []string { return s } res := func(s ...string) filterMatch { return simpleMatch(s) }
alt := func(m ...filterMatch) filterMatch { return alternationMatch(m) }
testCases := []struct { testCases := []struct {
pattern string pattern string
result []string result filterMatch
}{ }{
// Correct patterns // Correct patterns
// If a regexp pattern is correct, all split regexps need to be correct // If a regexp pattern is correct, all split regexps need to be correct
@ -49,6 +52,8 @@ func TestSplitRegexp(t *T) {
{`([)/][(])`, res(`([)/][(])`)}, {`([)/][(])`, res(`([)/][(])`)},
{"[(]/[)]", res("[(]", "[)]")}, {"[(]/[)]", res("[(]", "[)]")},
{"A/B|C/D", alt(res("A", "B"), res("C", "D"))},
// Faulty patterns // Faulty patterns
// Errors in original should produce at least one faulty regexp in results. // Errors in original should produce at least one faulty regexp in results.
{")/", res(")/")}, {")/", res(")/")},
@ -71,10 +76,8 @@ func TestSplitRegexp(t *T) {
// needs to have an error as well. // needs to have an error as well.
if _, err := regexp.Compile(tc.pattern); err != nil { if _, err := regexp.Compile(tc.pattern); err != nil {
ok := true ok := true
for _, re := range a { if err := a.verify("", regexp.MatchString); err != nil {
if _, err := regexp.Compile(re); err != nil { ok = false
ok = false
}
} }
if ok { if ok {
t.Errorf("%s: expected error in any of %q", tc.pattern, a) t.Errorf("%s: expected error in any of %q", tc.pattern, a)
@ -86,50 +89,75 @@ func TestSplitRegexp(t *T) {
func TestMatcher(t *T) { func TestMatcher(t *T) {
testCases := []struct { testCases := []struct {
pattern string pattern string
skip string
parent, sub string parent, sub string
ok bool ok bool
partial bool partial bool
}{ }{
// Behavior without subtests. // Behavior without subtests.
{"", "", "TestFoo", true, false}, {"", "", "", "TestFoo", true, false},
{"TestFoo", "", "TestFoo", true, false}, {"TestFoo", "", "", "TestFoo", true, false},
{"TestFoo/", "", "TestFoo", true, true}, {"TestFoo/", "", "", "TestFoo", true, true},
{"TestFoo/bar/baz", "", "TestFoo", true, true}, {"TestFoo/bar/baz", "", "", "TestFoo", true, true},
{"TestFoo", "", "TestBar", false, false}, {"TestFoo", "", "", "TestBar", false, false},
{"TestFoo/", "", "TestBar", false, false}, {"TestFoo/", "", "", "TestBar", false, false},
{"TestFoo/bar/baz", "", "TestBar/bar/baz", false, false}, {"TestFoo/bar/baz", "", "", "TestBar/bar/baz", false, false},
{"", "TestBar", "", "TestFoo", true, false},
{"", "TestBar", "", "TestBar", false, false},
// Skipping a non-existent test doesn't change anything.
{"", "TestFoo/skipped", "", "TestFoo", true, false},
{"TestFoo", "TestFoo/skipped", "", "TestFoo", true, false},
{"TestFoo/", "TestFoo/skipped", "", "TestFoo", true, true},
{"TestFoo/bar/baz", "TestFoo/skipped", "", "TestFoo", true, true},
{"TestFoo", "TestFoo/skipped", "", "TestBar", false, false},
{"TestFoo/", "TestFoo/skipped", "", "TestBar", false, false},
{"TestFoo/bar/baz", "TestFoo/skipped", "", "TestBar/bar/baz", false, false},
// with subtests // with subtests
{"", "TestFoo", "x", true, false}, {"", "", "TestFoo", "x", true, false},
{"TestFoo", "TestFoo", "x", true, false}, {"TestFoo", "", "TestFoo", "x", true, false},
{"TestFoo/", "TestFoo", "x", true, false}, {"TestFoo/", "", "TestFoo", "x", true, false},
{"TestFoo/bar/baz", "TestFoo", "bar", true, true}, {"TestFoo/bar/baz", "", "TestFoo", "bar", true, true},
{"", "TestFoo/skipped", "TestFoo", "x", true, false},
{"TestFoo", "TestFoo/skipped", "TestFoo", "x", true, false},
{"TestFoo", "TestFoo/skipped", "TestFoo", "skipped", false, false},
{"TestFoo/", "TestFoo/skipped", "TestFoo", "x", true, false},
{"TestFoo/bar/baz", "TestFoo/skipped", "TestFoo", "bar", true, true},
// Subtest with a '/' in its name still allows for copy and pasted names // Subtest with a '/' in its name still allows for copy and pasted names
// to match. // to match.
{"TestFoo/bar/baz", "TestFoo", "bar/baz", true, false}, {"TestFoo/bar/baz", "", "TestFoo", "bar/baz", true, false},
{"TestFoo/bar/baz", "TestFoo/bar", "baz", true, false}, {"TestFoo/bar/baz", "TestFoo/bar/baz", "TestFoo", "bar/baz", false, false},
{"TestFoo/bar/baz", "TestFoo", "x", false, false}, {"TestFoo/bar/baz", "TestFoo/bar/baz/skip", "TestFoo", "bar/baz", true, false},
{"TestFoo", "TestBar", "x", false, false}, {"TestFoo/bar/baz", "", "TestFoo/bar", "baz", true, false},
{"TestFoo/", "TestBar", "x", false, false}, {"TestFoo/bar/baz", "", "TestFoo", "x", false, false},
{"TestFoo/bar/baz", "TestBar", "x/bar/baz", false, false}, {"TestFoo", "", "TestBar", "x", false, false},
{"TestFoo/", "", "TestBar", "x", false, false},
{"TestFoo/bar/baz", "", "TestBar", "x/bar/baz", false, false},
{"A/B|C/D", "", "TestA", "B", true, false},
{"A/B|C/D", "", "TestC", "D", true, false},
{"A/B|C/D", "", "TestA", "C", false, false},
// subtests only // subtests only
{"", "TestFoo", "x", true, false}, {"", "", "TestFoo", "x", true, false},
{"/", "TestFoo", "x", true, false}, {"/", "", "TestFoo", "x", true, false},
{"./", "TestFoo", "x", true, false}, {"./", "", "TestFoo", "x", true, false},
{"./.", "TestFoo", "x", true, false}, {"./.", "", "TestFoo", "x", true, false},
{"/bar/baz", "TestFoo", "bar", true, true}, {"/bar/baz", "", "TestFoo", "bar", true, true},
{"/bar/baz", "TestFoo", "bar/baz", true, false}, {"/bar/baz", "", "TestFoo", "bar/baz", true, false},
{"//baz", "TestFoo", "bar/baz", true, false}, {"//baz", "", "TestFoo", "bar/baz", true, false},
{"//", "TestFoo", "bar/baz", true, false}, {"//", "", "TestFoo", "bar/baz", true, false},
{"/bar/baz", "TestFoo/bar", "baz", true, false}, {"/bar/baz", "", "TestFoo/bar", "baz", true, false},
{"//foo", "TestFoo", "bar/baz", false, false}, {"//foo", "", "TestFoo", "bar/baz", false, false},
{"/bar/baz", "TestFoo", "x", false, false}, {"/bar/baz", "", "TestFoo", "x", false, false},
{"/bar/baz", "TestBar", "x/bar/baz", false, false}, {"/bar/baz", "", "TestBar", "x/bar/baz", false, false},
} }
for _, tc := range testCases { for _, tc := range testCases {
m := newMatcher(regexp.MatchString, tc.pattern, "-test.run") m := newMatcher(regexp.MatchString, tc.pattern, "-test.run", tc.skip)
parent := &common{name: tc.parent} parent := &common{name: tc.parent}
if tc.parent != "" { if tc.parent != "" {
@ -142,45 +170,90 @@ func TestMatcher(t *T) {
} }
} }
func TestNaming(t *T) { var namingTestCases = []struct{ name, want string }{
m := newMatcher(regexp.MatchString, "", "") // Uniqueness
{"", "x/#00"},
{"", "x/#01"},
{"#0", "x/#0"}, // Doesn't conflict with #00 because the number of digits differs.
{"#00", "x/#00#01"}, // Conflicts with implicit #00 (used above), so add a suffix.
{"#", "x/#"},
{"#", "x/##01"},
{"t", "x/t"},
{"t", "x/t#01"},
{"t", "x/t#02"},
{"t#00", "x/t#00"}, // Explicit "#00" doesn't conflict with the unsuffixed first subtest.
{"a#01", "x/a#01"}, // user has subtest with this name.
{"a", "x/a"}, // doesn't conflict with this name.
{"a", "x/a#02"}, // This string is claimed now, so resume
{"a", "x/a#03"}, // with counting.
{"a#02", "x/a#02#01"}, // We already used a#02 once, so add a suffix.
{"b#00", "x/b#00"},
{"b", "x/b"}, // Implicit 0 doesn't conflict with explicit "#00".
{"b", "x/b#01"},
{"b#9223372036854775807", "x/b#9223372036854775807"}, // MaxInt64
{"b", "x/b#02"},
{"b", "x/b#03"},
// Sanitizing
{"A:1 B:2", "x/A:1_B:2"},
{"s\t\r\u00a0", "x/s___"},
{"\x01", `x/\x01`},
{"\U0010ffff", `x/\U0010ffff`},
}
func TestNaming(t *T) {
m := newMatcher(regexp.MatchString, "", "", "")
parent := &common{name: "x", level: 1} // top-level test. parent := &common{name: "x", level: 1} // top-level test.
// Rig the matcher with some preloaded values. for i, tc := range namingTestCases {
m.subNames["x/b"] = 1000
testCases := []struct {
name, want string
}{
// Uniqueness
{"", "x/#00"},
{"", "x/#01"},
{"t", "x/t"},
{"t", "x/t#01"},
{"t", "x/t#02"},
{"a#01", "x/a#01"}, // user has subtest with this name.
{"a", "x/a"}, // doesn't conflict with this name.
{"a", "x/a#01#01"}, // conflict, add disambiguating string.
{"a", "x/a#02"}, // This string is claimed now, so resume
{"a", "x/a#03"}, // with counting.
{"a#02", "x/a#02#01"},
{"b", "x/b#1000"}, // rigged, see above
{"b", "x/b#1001"},
// // Sanitizing
{"A:1 B:2", "x/A:1_B:2"},
{"s\t\r\u00a0", "x/s___"},
{"\x01", `x/\x01`},
{"\U0010ffff", `x/\U0010ffff`},
}
for i, tc := range testCases {
if got, _, _ := m.fullName(parent, tc.name); got != tc.want { if got, _, _ := m.fullName(parent, tc.name); got != tc.want {
t.Errorf("%d:%s: got %q; want %q", i, tc.name, got, tc.want) t.Errorf("%d:%s: got %q; want %q", i, tc.name, got, tc.want)
} }
} }
} }
func FuzzNaming(f *F) {
for _, tc := range namingTestCases {
f.Add(tc.name)
}
parent := &common{name: "x", level: 1}
var m *matcher
var seen map[string]string
reset := func() {
m = allMatcher()
seen = make(map[string]string)
}
reset()
f.Fuzz(func(t *T, subname string) {
if len(subname) > 10 {
// Long names attract the OOM killer.
t.Skip()
}
name := m.unique(parent.name, subname)
if !strings.Contains(name, "/"+subname) {
t.Errorf("name %q does not contain subname %q", name, subname)
}
if prev, ok := seen[name]; ok {
t.Errorf("name %q generated by both %q and %q", name, prev, subname)
}
if len(seen) > 1e6 {
// Free up memory.
reset()
}
seen[name] = subname
})
}
// GoString returns a string that is more readable than the default, which makes
// it easier to read test errors.
func (m alternationMatch) GoString() string {
s := make([]string, len(m))
for i, m := range m {
s[i] = fmt.Sprintf("%#v", m)
}
return fmt.Sprintf("(%s)", strings.Join(s, " | "))
}

Просмотреть файл

@ -499,7 +499,7 @@ func (m *M) Run() (code int) {
func runTests(matchString func(pat, str string) (bool, error), tests []InternalTest) (ran, ok bool) { func runTests(matchString func(pat, str string) (bool, error), tests []InternalTest) (ran, ok bool) {
ok = true ok = true
ctx := newTestContext(newMatcher(matchString, flagRunRegexp, "-test.run")) ctx := newTestContext(newMatcher(matchString, flagRunRegexp, "-test.run", ""))
t := &T{ t := &T{
common: common{ common: common{
output: &logger{logToStdout: flagVerbose}, output: &logger{logToStdout: flagVerbose},
@ -567,3 +567,14 @@ func MainStart(deps interface{}, tests []InternalTest, benchmarks []InternalBenc
deps: deps.(testDeps), deps: deps.(testDeps),
} }
} }
// A fake regexp matcher.
// Inflexible, but saves 50KB of flash and 50KB of RAM per -size full,
// and lets tests pass on cortex-m.
func fakeMatchString(pat, str string) (bool, error) {
if pat == ".*" {
return true, nil
}
matched := strings.Contains(str, pat)
return matched, nil
}