Commit a683c385 authored by Marcel van Lohuizen's avatar Marcel van Lohuizen

testing: add matching of subtest

Allows passing regexps per subtest to --test.run and --test.bench

Note that the documentation explicitly states that the split regular
expressions match the correpsonding parts (path components) of
the bench/test identifier. This is intended and slightly different
from the i'th RE matching the subtest/subbench at the respective
level.  Picking this semantics allows guaranteeing that a test or
benchmark identifier as printed by go test can be passed verbatim
(possibly quoted) to, respectively, -run or -bench: subtests and
subbenches might have a '/' in their name, causing a misaligment if
their ID is passed to -run or -bench as is.
This semantics has other benefits, but this is the main motivation.

Fixes golang.go#15126

Change-Id: If72e6d3f54db1df6bc2729ac6edc7ab3c740e7c3
Reviewed-on: https://go-review.googlesource.com/19122Reviewed-by: default avatarRuss Cox <rsc@golang.org>
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent f8fc3710
...@@ -1348,9 +1348,12 @@ The following flags are recognized by the 'go test' command and ...@@ -1348,9 +1348,12 @@ The following flags are recognized by the 'go test' command and
control the execution of any test: control the execution of any test:
-bench regexp -bench regexp
Run benchmarks matching the regular expression. Run (sub)benchmarks matching a regular expression.
By default, no benchmarks run. The given regular expression is split into smaller ones by
To run all benchmarks, use '-bench=.'. top-level '/', where each must match the corresponding part of a
benchmark's identifier.
By default, no benchmarks run. To run all benchmarks,
use '-bench .' or '-bench=.'.
-benchmem -benchmem
Print memory allocation statistics for benchmarks. Print memory allocation statistics for benchmarks.
...@@ -1436,10 +1439,10 @@ control the execution of any test: ...@@ -1436,10 +1439,10 @@ control the execution of any test:
(see 'go help build'). (see 'go help build').
-run regexp -run regexp
Run only those tests and examples matching the regular Run only those tests and examples matching the regular expression.
expression. By default, all tests run. For tests the regular expression is split into smaller ones by
To skip all tests, use a pattern that matches no test names, top-level '/', where each must match the corresponding part of a
such as '-run=^$'. test's identifier.
-short -short
Tell long-running tests to shorten their run time. Tell long-running tests to shorten their run time.
......
...@@ -125,9 +125,12 @@ control the execution of any test: ...@@ -125,9 +125,12 @@ control the execution of any test:
const testFlag2 = ` const testFlag2 = `
-bench regexp -bench regexp
Run benchmarks matching the regular expression. Run (sub)benchmarks matching a regular expression.
By default, no benchmarks run. The given regular expression is split into smaller ones by
To run all benchmarks, use '-bench=.'. top-level '/', where each must match the corresponding part of a
benchmark's identifier.
By default, no benchmarks run. To run all benchmarks,
use '-bench .' or '-bench=.'.
-benchmem -benchmem
Print memory allocation statistics for benchmarks. Print memory allocation statistics for benchmarks.
...@@ -213,10 +216,10 @@ const testFlag2 = ` ...@@ -213,10 +216,10 @@ const testFlag2 = `
(see 'go help build'). (see 'go help build').
-run regexp -run regexp
Run only those tests and examples matching the regular Run only those tests and examples matching the regular expression.
expression. By default, all tests run. For tests the regular expression is split into smaller ones by
To skip all tests, use a pattern that matches no test names, top-level '/', where each must match the corresponding part of a
such as '-run=^$'. test's identifier.
-short -short
Tell long-running tests to shorten their run time. Tell long-running tests to shorten their run time.
......
...@@ -8,29 +8,40 @@ import ( ...@@ -8,29 +8,40 @@ import (
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"strings"
"sync" "sync"
) )
// 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 []string
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 map[string]int64
} }
// TODO: fix test_main to avoid race and improve caching. // TODO: fix test_main to avoid race and improve caching, also allowing to
// eliminate this Mutex.
var matchMutex sync.Mutex var matchMutex sync.Mutex
func newMatcher(matchString func(pat, str string) (bool, error), pattern, name string) *matcher { func newMatcher(matchString func(pat, str string) (bool, error), patterns, name string) *matcher {
// Verify filters before doing any processing. var filter []string
if _, err := matchString(pattern, "non-empty"); err != nil { if patterns != "" {
fmt.Fprintf(os.Stderr, "testing: invalid regexp for %s: %s\n", name, err) filter = splitRegexp(patterns)
os.Exit(1) for i, s := range filter {
filter[i] = rewrite(s)
}
// Verify filters before doing any processing.
for i, s := range filter {
if _, err := matchString(s, "non-empty"); err != nil {
fmt.Fprintf(os.Stderr, "testing: invalid regexp for element %d of %s (%q): %s\n", i, name, s, err)
os.Exit(1)
}
}
} }
return &matcher{ return &matcher{
filter: pattern, filter: filter,
matchFunc: matchString, matchFunc: matchString,
subNames: map[string]int64{}, subNames: map[string]int64{},
} }
...@@ -49,14 +60,54 @@ func (m *matcher) fullName(c *common, subname string) (name string, ok bool) { ...@@ -49,14 +60,54 @@ func (m *matcher) fullName(c *common, subname string) (name string, ok bool) {
matchMutex.Lock() matchMutex.Lock()
defer matchMutex.Unlock() defer matchMutex.Unlock()
if c != nil && c.level == 0 { // We check the full array of paths each time to allow for the case that
if matched, _ := m.matchFunc(m.filter, subname); !matched { // a pattern contains a '/'.
for i, s := range strings.Split(name, "/") {
if i >= len(m.filter) {
break
}
if ok, _ := m.matchFunc(m.filter[i], s); !ok {
return name, false return name, false
} }
} }
return name, true return name, true
} }
func splitRegexp(s string) []string {
a := make([]string, 0, strings.Count(s, "/"))
cs := 0
cp := 0
for i := 0; i < len(s); {
switch s[i] {
case '[':
cs++
case ']':
if cs--; cs < 0 { // An unmatched ']' is legal.
cs = 0
}
case '(':
if cs == 0 {
cp++
}
case ')':
if cs == 0 {
cp--
}
case '\\':
i++
case '/':
if cs == 0 && cp == 0 {
a = append(a, s[:i])
s = s[i+1:]
i = 0
continue
}
}
i++
}
return append(a, s)
}
// 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 ore more counts, if necessary. // with one ore more counts, if necessary.
func (m *matcher) unique(parent, subname string) string { func (m *matcher) unique(parent, subname string) string {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package testing package testing
import ( import (
"reflect"
"regexp" "regexp"
"unicode" "unicode"
) )
...@@ -23,6 +24,123 @@ func TestIsSpace(t *T) { ...@@ -23,6 +24,123 @@ func TestIsSpace(t *T) {
} }
} }
func TestSplitRegexp(t *T) {
res := func(s ...string) []string { return s }
testCases := []struct {
pattern string
result []string
}{
// Correct patterns
// If a regexp pattern is correct, all split regexps need to be correct
// as well.
{"", res("")},
{"/", res("", "")},
{"//", res("", "", "")},
{"A", res("A")},
{"A/B", res("A", "B")},
{"A/B/", res("A", "B", "")},
{"/A/B/", res("", "A", "B", "")},
{"[A]/(B)", res("[A]", "(B)")},
{"[/]/[/]", res("[/]", "[/]")},
{"[/]/[:/]", res("[/]", "[:/]")},
{"/]", res("", "]")},
{"]/", res("]", "")},
{"]/[/]", res("]", "[/]")},
{`([)/][(])`, res(`([)/][(])`)},
{"[(]/[)]", res("[(]", "[)]")},
// Faulty patterns
// Errors in original should produce at least one faulty regexp in results.
{")/", res(")/")},
{")/(/)", res(")/(", ")")},
{"a[/)b", res("a[/)b")},
{"(/]", res("(/]")},
{"(/", res("(/")},
{"[/]/[/", res("[/]", "[/")},
{`\p{/}`, res(`\p{`, "}")},
{`\p/`, res(`\p`, "")},
{`[[:/:]]`, res(`[[:/:]]`)},
}
for _, tc := range testCases {
a := splitRegexp(tc.pattern)
if !reflect.DeepEqual(a, tc.result) {
t.Errorf("splitRegexp(%q) = %#v; want %#v", tc.pattern, a, tc.result)
}
// If there is any error in the pattern, one of the returned subpatterns
// needs to have an error as well.
if _, err := regexp.Compile(tc.pattern); err != nil {
ok := true
for _, re := range a {
if _, err := regexp.Compile(re); err != nil {
ok = false
}
}
if ok {
t.Errorf("%s: expected error in any of %q", tc.pattern, a)
}
}
}
}
func TestMatcher(t *T) {
testCases := []struct {
pattern string
parent, sub string
ok bool
}{
// Behavior without subtests.
{"", "", "TestFoo", true},
{"TestFoo", "", "TestFoo", true},
{"TestFoo/", "", "TestFoo", true},
{"TestFoo/bar/baz", "", "TestFoo", true},
{"TestFoo", "", "TestBar", false},
{"TestFoo/", "", "TestBar", false},
{"TestFoo/bar/baz", "", "TestBar/bar/baz", false},
// with subtests
{"", "TestFoo", "x", true},
{"TestFoo", "TestFoo", "x", true},
{"TestFoo/", "TestFoo", "x", true},
{"TestFoo/bar/baz", "TestFoo", "bar", true},
// Subtest with a '/' in its name still allows for copy and pasted names
// to match.
{"TestFoo/bar/baz", "TestFoo", "bar/baz", true},
{"TestFoo/bar/baz", "TestFoo/bar", "baz", true},
{"TestFoo/bar/baz", "TestFoo", "x", false},
{"TestFoo", "TestBar", "x", false},
{"TestFoo/", "TestBar", "x", false},
{"TestFoo/bar/baz", "TestBar", "x/bar/baz", false},
// subtests only
{"", "TestFoo", "x", true},
{"/", "TestFoo", "x", true},
{"./", "TestFoo", "x", true},
{"./.", "TestFoo", "x", true},
{"/bar/baz", "TestFoo", "bar", true},
{"/bar/baz", "TestFoo", "bar/baz", true},
{"//baz", "TestFoo", "bar/baz", true},
{"//", "TestFoo", "bar/baz", true},
{"/bar/baz", "TestFoo/bar", "baz", true},
{"//foo", "TestFoo", "bar/baz", false},
{"/bar/baz", "TestFoo", "x", false},
{"/bar/baz", "TestBar", "x/bar/baz", false},
}
for _, tc := range testCases {
m := newMatcher(regexp.MatchString, tc.pattern, "-test.run")
parent := &common{name: tc.parent}
if tc.parent != "" {
parent.level = 1
}
if n, ok := m.fullName(parent, tc.sub); ok != tc.ok {
t.Errorf("pattern: %q, parent: %q, sub %q: got %v; want %v",
tc.pattern, tc.parent, tc.sub, ok, tc.ok, n)
}
}
}
func TestNaming(t *T) { func TestNaming(t *T) {
m := newMatcher(regexp.MatchString, "", "") m := newMatcher(regexp.MatchString, "", "")
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment