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 }