github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/internal/testing/matcher.go (about)

     1  // Copyright 2021 The ChromiumOS Authors
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package testing
     6  
     7  import (
     8  	"fmt"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  	"unicode"
    13  
    14  	"go.chromium.org/tast/core/internal/expr"
    15  )
    16  
    17  // Matcher holds compiled patterns to match tests.
    18  type Matcher struct {
    19  	names map[string]struct{}
    20  	globs map[string]*regexp.Regexp
    21  	exprs []*expr.Expr
    22  }
    23  
    24  // NewMatcher creates a new Matcher from patterns.
    25  func NewMatcher(pats []string) (*Matcher, error) {
    26  	if len(pats) == 1 && strings.HasPrefix(pats[0], "(") && strings.HasSuffix(pats[0], ")") {
    27  		return compileExpr(pats[0][1 : len(pats[0])-1])
    28  	}
    29  	return compileGlobs(pats)
    30  }
    31  
    32  // Match matches a test.
    33  func (m *Matcher) Match(name string, attrs []string) bool {
    34  	if _, ok := m.names[name]; ok {
    35  		return true
    36  	}
    37  	for _, g := range m.globs {
    38  		if g.MatchString(name) {
    39  			return true
    40  		}
    41  	}
    42  	for _, e := range m.exprs {
    43  		if e.Matches(attrs) {
    44  			return true
    45  		}
    46  	}
    47  	return false
    48  }
    49  
    50  func compileExpr(s string) (*Matcher, error) {
    51  	e, err := expr.New(s)
    52  	if err != nil {
    53  		return nil, fmt.Errorf("bad expr: %v", err)
    54  	}
    55  	return &Matcher{exprs: []*expr.Expr{e}}, nil
    56  }
    57  
    58  func compileGlobs(pats []string) (*Matcher, error) {
    59  	// If the pattern is empty, return a matcher that matches anything.
    60  	if len(pats) == 0 {
    61  		pats = []string{"*"}
    62  	}
    63  	// Print a helpful error message if it looks like the user wanted an attribute expression.
    64  	if len(pats) == 1 && (strings.Contains(pats[0], "&&") || strings.Contains(pats[0], "||")) {
    65  		return nil, fmt.Errorf("attr expr %q must be within parentheses", pats[0])
    66  	}
    67  
    68  	names := make(map[string]struct{})
    69  	globs := make(map[string]*regexp.Regexp)
    70  	for _, pat := range pats {
    71  		hasWildcard, err := validateGlob(pat)
    72  		if err != nil {
    73  			return nil, err
    74  		}
    75  		if hasWildcard {
    76  			glob, err := compileGlob(pat)
    77  			if err != nil {
    78  				return nil, err
    79  			}
    80  			globs[pat] = glob
    81  		} else {
    82  			names[pat] = struct{}{}
    83  		}
    84  	}
    85  	return &Matcher{names: names, globs: globs}, nil
    86  }
    87  
    88  // validateGlob checks if glob is a valid glob. It also returns if pat contains
    89  // wildcards.
    90  func validateGlob(glob string) (hasWildcard bool, err error) {
    91  	for _, ch := range glob {
    92  		switch {
    93  		case ch == '*':
    94  			hasWildcard = true
    95  		case unicode.IsLetter(ch), unicode.IsDigit(ch), ch == '.', ch == '_':
    96  			continue
    97  		default:
    98  			return hasWildcard, fmt.Errorf("invalid character %q in pattern %v", ch, glob)
    99  		}
   100  	}
   101  	return hasWildcard, nil
   102  }
   103  
   104  // compileGlob returns a compiled regular expression corresponding to glob.
   105  // glob must be verified in advance with validateGlob.
   106  func compileGlob(glob string) (*regexp.Regexp, error) {
   107  	glob = strings.Replace(glob, ".", "\\.", -1)
   108  	glob = strings.Replace(glob, "*", ".*", -1)
   109  	glob = "^" + glob + "$"
   110  	return regexp.Compile(glob)
   111  }
   112  
   113  // NewTestGlobRegexp returns a compiled regular expression corresponding to
   114  // glob.
   115  //
   116  // DEPRECATED: Use Matcher instead.
   117  func NewTestGlobRegexp(glob string) (*regexp.Regexp, error) {
   118  	if _, err := validateGlob(glob); err != nil {
   119  		return nil, err
   120  	}
   121  	return compileGlob(glob)
   122  }
   123  
   124  // UnmatchedPatterns returns a list of test name patterns (exact or wildcards) in the matcher that do not match any of supplied test names.
   125  // This method always returns nil if the pattern in the matcher is an attribute expression.
   126  func (m *Matcher) UnmatchedPatterns(tests []string) []string {
   127  	if len(m.exprs) > 0 {
   128  		return nil
   129  	}
   130  
   131  	matched := make(map[string]struct{})
   132  	for k, g := range m.globs {
   133  		for _, t := range tests {
   134  			if g.MatchString(t) {
   135  				matched[k] = struct{}{}
   136  				break
   137  			}
   138  		}
   139  	}
   140  	for _, t := range tests {
   141  		if _, ok := m.names[t]; ok {
   142  			matched[t] = struct{}{}
   143  		}
   144  	}
   145  
   146  	var notFoundList []string
   147  	for k := range m.globs {
   148  		if _, ok := matched[k]; !ok {
   149  			notFoundList = append(notFoundList, k)
   150  		}
   151  	}
   152  	for n := range m.names {
   153  		if _, ok := matched[n]; !ok {
   154  			notFoundList = append(notFoundList, n)
   155  		}
   156  	}
   157  
   158  	sort.Strings(notFoundList)
   159  	return notFoundList
   160  }