mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
testing: added name matcher and sanitizer
The matcher is responsible for sanitizing and uniquing the test and benchmark names and thus needs to be included before the API can be exposed. Matching currently uses the regexp to only match the top-level tests/benchmarks. Support for subtest matching is for another CL. Change-Id: I7c8464068faef7ebc179b03a7fe3d01122cc4f0b Reviewed-on: https://go-review.googlesource.com/18897 Reviewed-by: Russ Cox <rsc@golang.org> Run-TryBot: Marcel van Lohuizen <mpvl@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
parent
34699bc7a8
commit
00a2a94c1e
5 changed files with 207 additions and 26 deletions
|
|
@ -338,6 +338,8 @@ func benchmarkName(name string, n int) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type benchContext struct {
|
type benchContext struct {
|
||||||
|
match *matcher
|
||||||
|
|
||||||
maxLen int // The largest recorded benchmark name.
|
maxLen int // The largest recorded benchmark name.
|
||||||
extLen int // Maximum extension length.
|
extLen int // Maximum extension length.
|
||||||
}
|
}
|
||||||
|
|
@ -361,16 +363,12 @@ func runBenchmarksInternal(matchString func(pat, str string) (bool, error), benc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx := &benchContext{
|
ctx := &benchContext{
|
||||||
|
match: newMatcher(matchString, *matchBenchmarks, "-test.bench"),
|
||||||
extLen: len(benchmarkName("", maxprocs)),
|
extLen: len(benchmarkName("", maxprocs)),
|
||||||
}
|
}
|
||||||
var bs []InternalBenchmark
|
var bs []InternalBenchmark
|
||||||
for _, Benchmark := range benchmarks {
|
for _, Benchmark := range benchmarks {
|
||||||
matched, err := matchString(*matchBenchmarks, Benchmark.Name)
|
if _, matched := ctx.match.fullName(nil, Benchmark.Name); matched {
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "testing: invalid regexp for -test.bench: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if matched {
|
|
||||||
bs = append(bs, Benchmark)
|
bs = append(bs, Benchmark)
|
||||||
benchName := benchmarkName(Benchmark.Name, maxprocs)
|
benchName := benchmarkName(Benchmark.Name, maxprocs)
|
||||||
if l := len(benchName) + ctx.extLen + 1; l > ctx.maxLen {
|
if l := len(benchName) + ctx.extLen + 1; l > ctx.maxLen {
|
||||||
|
|
@ -443,13 +441,17 @@ func (b *B) runBench(name string, f func(b *B)) bool {
|
||||||
benchmarkLock.Unlock()
|
benchmarkLock.Unlock()
|
||||||
defer benchmarkLock.Lock()
|
defer benchmarkLock.Lock()
|
||||||
|
|
||||||
if b.level > 0 {
|
benchName, ok := b.name, true
|
||||||
name = b.name + "/" + name
|
if b.context != nil {
|
||||||
|
benchName, ok = b.context.match.fullName(&b.common, name)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
sub := &B{
|
sub := &B{
|
||||||
common: common{
|
common: common{
|
||||||
signal: make(chan bool),
|
signal: make(chan bool),
|
||||||
name: name,
|
name: benchName,
|
||||||
parent: &b.common,
|
parent: &b.common,
|
||||||
level: b.level + 1,
|
level: b.level + 1,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
116
src/testing/match.go
Normal file
116
src/testing/match.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package testing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// matcher sanitizes, uniques, and filters names of subtests and subbenchmarks.
|
||||||
|
type matcher struct {
|
||||||
|
filter string
|
||||||
|
matchFunc func(pat, str string) (bool, error)
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
subNames map[string]int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fix test_main to avoid race and improve caching.
|
||||||
|
var matchMutex sync.Mutex
|
||||||
|
|
||||||
|
func newMatcher(matchString func(pat, str string) (bool, error), pattern, name string) *matcher {
|
||||||
|
// Verify filters before doing any processing.
|
||||||
|
if _, err := matchString(pattern, "non-empty"); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "testing: invalid regexp for %s: %s\n", name, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return &matcher{
|
||||||
|
filter: pattern,
|
||||||
|
matchFunc: matchString,
|
||||||
|
subNames: map[string]int64{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *matcher) fullName(c *common, subname string) (name string, ok bool) {
|
||||||
|
name = subname
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if c != nil && c.level > 0 {
|
||||||
|
name = m.unique(c.name, rewrite(subname))
|
||||||
|
}
|
||||||
|
|
||||||
|
matchMutex.Lock()
|
||||||
|
defer matchMutex.Unlock()
|
||||||
|
|
||||||
|
if c != nil && c.level == 0 {
|
||||||
|
if matched, _ := m.matchFunc(m.filter, subname); !matched {
|
||||||
|
return name, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// unique creates a unique name for the given parent and subname by affixing it
|
||||||
|
// with one ore more counts, if necessary.
|
||||||
|
func (m *matcher) unique(parent, subname string) string {
|
||||||
|
name := fmt.Sprintf("%s/%s", 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.
|
||||||
|
name = fmt.Sprintf("%s#%02d", name, next)
|
||||||
|
empty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewrite rewrites a subname to having only printable characters and no white
|
||||||
|
// space.
|
||||||
|
func rewrite(s string) string {
|
||||||
|
b := []byte{}
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case isSpace(r):
|
||||||
|
b = append(b, '_')
|
||||||
|
case !strconv.IsPrint(r):
|
||||||
|
s := strconv.QuoteRune(r)
|
||||||
|
b = append(b, s[1:len(s)-1]...)
|
||||||
|
default:
|
||||||
|
b = append(b, string(r)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpace(r rune) bool {
|
||||||
|
if r < 0x2000 {
|
||||||
|
switch r {
|
||||||
|
// Note: not the same as Unicode Z class.
|
||||||
|
case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0, 0x1680:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if r <= 0x200a {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch r {
|
||||||
|
case 0x2028, 0x2029, 0x202f, 0x205f, 0x3000:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
67
src/testing/match_test.go
Normal file
67
src/testing/match_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package testing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify that our IsSpace agrees with unicode.IsSpace.
|
||||||
|
func TestIsSpace(t *T) {
|
||||||
|
n := 0
|
||||||
|
for r := rune(0); r <= unicode.MaxRune; r++ {
|
||||||
|
if isSpace(r) != unicode.IsSpace(r) {
|
||||||
|
t.Errorf("IsSpace(%U)=%t incorrect", r, isSpace(r))
|
||||||
|
n++
|
||||||
|
if n > 10 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNaming(t *T) {
|
||||||
|
m := newMatcher(regexp.MatchString, "", "")
|
||||||
|
|
||||||
|
parent := &common{name: "x", level: 1} // top-level test.
|
||||||
|
|
||||||
|
// Rig the matcher with some preloaded values.
|
||||||
|
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 {
|
||||||
|
t.Errorf("%d:%s: got %q; want %q", i, tc.name, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ package testing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -305,11 +306,12 @@ func TestTRun(t *T) {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
ctx := newTestContext(tc.maxPar)
|
ctx := newTestContext(tc.maxPar, newMatcher(regexp.MatchString, "", ""))
|
||||||
root := &T{
|
root := &T{
|
||||||
common: common{
|
common: common{
|
||||||
barrier: make(chan bool),
|
signal: make(chan bool),
|
||||||
w: ioutil.Discard,
|
name: "Test",
|
||||||
|
w: ioutil.Discard,
|
||||||
},
|
},
|
||||||
context: ctx,
|
context: ctx,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -551,9 +551,9 @@ func tRunner(t *T, fn func(t *T)) {
|
||||||
// run runs f as a subtest of t called name. It reports whether f succeeded.
|
// run runs f as a subtest of t called name. It reports whether f succeeded.
|
||||||
// Run will block until all its parallel subtests have completed.
|
// Run will block until all its parallel subtests have completed.
|
||||||
func (t *T) run(name string, f func(t *T)) bool {
|
func (t *T) run(name string, f func(t *T)) bool {
|
||||||
testName := name
|
testName, ok := t.context.match.fullName(&t.common, name)
|
||||||
if t.level > 0 {
|
if !ok {
|
||||||
testName = t.name + "/" + name
|
return true
|
||||||
}
|
}
|
||||||
t = &T{
|
t = &T{
|
||||||
common: common{
|
common: common{
|
||||||
|
|
@ -583,6 +583,8 @@ func (t *T) run(name string, f func(t *T)) bool {
|
||||||
// testContext holds all fields that are common to all tests. This includes
|
// testContext holds all fields that are common to all tests. This includes
|
||||||
// synchronization primitives to run at most *parallel tests.
|
// synchronization primitives to run at most *parallel tests.
|
||||||
type testContext struct {
|
type testContext struct {
|
||||||
|
match *matcher
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
// Channel used to signal tests that are ready to be run in parallel.
|
// Channel used to signal tests that are ready to be run in parallel.
|
||||||
|
|
@ -599,8 +601,9 @@ type testContext struct {
|
||||||
maxParallel int
|
maxParallel int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestContext(maxParallel int) *testContext {
|
func newTestContext(maxParallel int, m *matcher) *testContext {
|
||||||
return &testContext{
|
return &testContext{
|
||||||
|
match: m,
|
||||||
startParallel: make(chan bool),
|
startParallel: make(chan bool),
|
||||||
maxParallel: maxParallel,
|
maxParallel: maxParallel,
|
||||||
running: 1, // Set the count to 1 for the main (sequential) test.
|
running: 1, // Set the count to 1 for the main (sequential) test.
|
||||||
|
|
@ -707,7 +710,7 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
|
||||||
}
|
}
|
||||||
for _, procs := range cpuList {
|
for _, procs := range cpuList {
|
||||||
runtime.GOMAXPROCS(procs)
|
runtime.GOMAXPROCS(procs)
|
||||||
ctx := newTestContext(*parallel)
|
ctx := newTestContext(*parallel, newMatcher(matchString, *match, "-test.run"))
|
||||||
t := &T{
|
t := &T{
|
||||||
common: common{
|
common: common{
|
||||||
signal: make(chan bool),
|
signal: make(chan bool),
|
||||||
|
|
@ -718,15 +721,6 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
|
||||||
}
|
}
|
||||||
tRunner(t, func(t *T) {
|
tRunner(t, func(t *T) {
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
// TODO: a version of this will be the Run method.
|
|
||||||
matched, err := matchString(*match, test.Name)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "testing: invalid regexp for -test.run: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if !matched {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.run(test.Name, test.F)
|
t.run(test.Name, test.F)
|
||||||
}
|
}
|
||||||
// Run catching the signal rather than the tRunner as a separate
|
// Run catching the signal rather than the tRunner as a separate
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue