blob: c70efd1df64c29c0955ff950a6bc10680fbbd546 [file]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package testing
import (
"fmt"
"regexp"
"sort"
"strings"
"unicode"
"go.chromium.org/tast/core/internal/expr"
)
// Matcher holds compiled patterns to match tests.
type Matcher struct {
names map[string]struct{}
globs map[string]*regexp.Regexp
exprs []*expr.Expr
}
// NewMatcher creates a new Matcher from patterns.
func NewMatcher(pats []string) (*Matcher, error) {
if len(pats) == 1 && strings.HasPrefix(pats[0], "(") && strings.HasSuffix(pats[0], ")") {
return compileExpr(pats[0][1 : len(pats[0])-1])
}
return compileGlobs(pats)
}
// Match matches a test.
func (m *Matcher) Match(name string, attrs []string) bool {
if _, ok := m.names[name]; ok {
return true
}
for _, g := range m.globs {
if g.MatchString(name) {
return true
}
}
for _, e := range m.exprs {
if e.Matches(attrs) {
return true
}
}
return false
}
func compileExpr(s string) (*Matcher, error) {
e, err := expr.New(s)
if err != nil {
return nil, fmt.Errorf("bad expr: %v", err)
}
return &Matcher{exprs: []*expr.Expr{e}}, nil
}
func compileGlobs(pats []string) (*Matcher, error) {
// If the pattern is empty, return a matcher that matches anything.
if len(pats) == 0 {
pats = []string{"*"}
}
// Print a helpful error message if it looks like the user wanted an attribute expression.
if len(pats) == 1 && (strings.Contains(pats[0], "&&") || strings.Contains(pats[0], "||")) {
return nil, fmt.Errorf("attr expr %q must be within parentheses", pats[0])
}
names := make(map[string]struct{})
globs := make(map[string]*regexp.Regexp)
for _, pat := range pats {
hasWildcard, err := validateGlob(pat)
if err != nil {
return nil, err
}
if hasWildcard {
glob, err := compileGlob(pat)
if err != nil {
return nil, err
}
globs[pat] = glob
} else {
names[pat] = struct{}{}
}
}
return &Matcher{names: names, globs: globs}, nil
}
// validateGlob checks if glob is a valid glob. It also returns if pat contains
// wildcards.
func validateGlob(glob string) (hasWildcard bool, err error) {
for _, ch := range glob {
switch {
case ch == '*':
hasWildcard = true
case unicode.IsLetter(ch), unicode.IsDigit(ch), ch == '.', ch == '_':
continue
default:
return hasWildcard, fmt.Errorf("invalid character %q in pattern %v", ch, glob)
}
}
return hasWildcard, nil
}
// compileGlob returns a compiled regular expression corresponding to glob.
// glob must be verified in advance with validateGlob.
func compileGlob(glob string) (*regexp.Regexp, error) {
glob = strings.Replace(glob, ".", "\\.", -1)
glob = strings.Replace(glob, "*", ".*", -1)
glob = "^" + glob + "$"
return regexp.Compile(glob)
}
// NewTestGlobRegexp returns a compiled regular expression corresponding to
// glob.
//
// DEPRECATED: Use Matcher instead.
func NewTestGlobRegexp(glob string) (*regexp.Regexp, error) {
if _, err := validateGlob(glob); err != nil {
return nil, err
}
return compileGlob(glob)
}
// UnmatchedPatterns returns a list of test name patterns (exact or wildcards) in the matcher that do not match any of supplied test names.
// This method always returns nil if the pattern in the matcher is an attribute expression.
func (m *Matcher) UnmatchedPatterns(tests []string) []string {
if len(m.exprs) > 0 {
return nil
}
matched := make(map[string]struct{})
for k, g := range m.globs {
for _, t := range tests {
if g.MatchString(t) {
matched[k] = struct{}{}
break
}
}
}
for _, t := range tests {
if _, ok := m.names[t]; ok {
matched[t] = struct{}{}
}
}
var notFoundList []string
for k := range m.globs {
if _, ok := matched[k]; !ok {
notFoundList = append(notFoundList, k)
}
}
for n := range m.names {
if _, ok := matched[n]; !ok {
notFoundList = append(notFoundList, n)
}
}
sort.Strings(notFoundList)
return notFoundList
}