github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/eval.go (about) 1 /* 2 Copyright 2022 The TestGrid Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package updater 18 19 import ( 20 "regexp" 21 "strings" 22 "sync" 23 24 "github.com/GoogleCloudPlatform/testgrid/metadata/junit" 25 evalpb "github.com/GoogleCloudPlatform/testgrid/pb/custom_evaluator" 26 tspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status" 27 ) 28 29 var ( 30 res = map[string]*regexp.Regexp{} // cache regexps for more speed 31 resLock sync.RWMutex 32 ) 33 34 func strReg(val, expr string) bool { 35 resLock.RLock() 36 r, ok := res[expr] 37 resLock.RUnlock() 38 if !ok { 39 var err error 40 if r, err = regexp.Compile(expr); err != nil { 41 r = nil 42 } 43 resLock.Lock() 44 res[expr] = r 45 resLock.Unlock() 46 } 47 if r == nil { 48 return false 49 } 50 return r.MatchString(val) 51 } 52 53 func strEQ(a, b string) bool { return a == b } 54 func strNE(a, b string) bool { return a != b } 55 56 func strStartsWith(a, b string) bool { 57 return strings.HasPrefix(b, a) 58 } 59 60 func strContains(a, b string) bool { 61 return strings.Contains(a, b) 62 } 63 64 func numEQ(a, b float64) bool { 65 return a == b 66 } 67 68 func numNE(a, b float64) bool { 69 return a != b 70 } 71 72 func targetStatusEQ(a, b tspb.TestStatus) bool { 73 return a == b 74 } 75 76 // TestResult defines the interface for accessing data about the result. 77 type TestResult interface { 78 // Properties for the test result. 79 Properties() map[string][]string 80 // Name of the test case 81 Name() string 82 // The number of errors in the test case. 83 Errors() int 84 // The number of failures in the test case. 85 Failures() int 86 // The sequence of exception/error messages in the test case. 87 Exceptions() []string 88 } 89 90 // TargetResult defines the interface for accessing data about the target/suite result. 91 type TargetResult interface { 92 TargetStatus() tspb.TestStatus 93 } 94 95 // CustomStatus evaluates the result according to the rules. 96 // 97 // Returns nil if no rule matches, otherwise returns the overridden status. 98 func CustomStatus(rules []*evalpb.Rule, testResult TestResult) *tspb.TestStatus { 99 for _, rule := range rules { 100 if got := evalProperties(rule, testResult); got != nil { 101 return got 102 } 103 } 104 return nil 105 } 106 107 // CustomTargetStatus evaluates the result according to the rules. 108 // 109 // Returns nil if no rule matches, otherwise returns the overridden status. 110 func CustomTargetStatus(rules []*evalpb.Rule, targetResult TargetResult) *tspb.TestStatus { 111 for _, rule := range rules { 112 if got := evalTargetProperties(rule, targetResult); got != nil { 113 return got 114 } 115 } 116 return nil 117 } 118 119 type jUnitTestResult struct { 120 Result *junit.Result 121 } 122 123 func (jr jUnitTestResult) Properties() map[string][]string { 124 if jr.Result == nil || jr.Result.Properties == nil || jr.Result.Properties.PropertyList == nil { 125 return nil 126 } 127 out := make(map[string][]string, len(jr.Result.Properties.PropertyList)) 128 for _, p := range jr.Result.Properties.PropertyList { 129 out[p.Name] = append(out[p.Name], p.Value) 130 } 131 return out 132 } 133 134 func (jr jUnitTestResult) Name() string { 135 return jr.Result.Name 136 } 137 138 func (jr jUnitTestResult) Errors() int { 139 if jr.Result.Errored != nil { 140 return 1 141 } 142 return 0 143 } 144 145 func (jr jUnitTestResult) Failures() int { 146 if jr.Result.Failure != nil { 147 return 1 148 } 149 return 0 150 } 151 152 func (jr jUnitTestResult) Exceptions() []string { 153 options := make([]string, 0, 2) 154 if e := jr.Result.Errored; e != nil { 155 options = append(options, e.Value) 156 } 157 if f := jr.Result.Failure; f != nil { 158 options = append(options, f.Value) 159 } 160 return options 161 } 162 163 func evalProperties(rule *evalpb.Rule, testResult TestResult) *tspb.TestStatus { 164 for _, cmp := range rule.TestResultComparisons { 165 if cmp.Comparison == nil { 166 return nil 167 } 168 var scmp func(string, string) bool 169 var fcmp func(float64, float64) bool 170 sval := cmp.Comparison.GetStringValue() 171 fval := cmp.Comparison.GetNumericalValue() 172 switch cmp.Comparison.Op { 173 case evalpb.Comparison_OP_CONTAINS: 174 scmp = strContains 175 case evalpb.Comparison_OP_REGEX: 176 scmp = strReg 177 case evalpb.Comparison_OP_STARTS_WITH: 178 scmp = strStartsWith 179 case evalpb.Comparison_OP_EQ: 180 scmp = strEQ 181 fcmp = numEQ 182 case evalpb.Comparison_OP_NE: 183 scmp = strNE 184 fcmp = numNE 185 } 186 187 if p := cmp.GetPropertyKey(); p != "" { 188 if scmp == nil { 189 return nil 190 } 191 var good bool 192 props := testResult.Properties() 193 if props == nil { 194 return nil 195 } 196 for _, val := range props[p] { 197 if !scmp(val, sval) { 198 continue 199 } 200 good = true 201 break 202 } 203 if !good { 204 return nil 205 } 206 } else if f := cmp.GetTestResultField(); f != "" { 207 var getNum func() int 208 var getStr func() string 209 switch f { 210 case "name": 211 getStr = testResult.Name 212 case "error_count": 213 getNum = testResult.Errors 214 case "failure_count": 215 getNum = testResult.Failures 216 default: // TODO(fejta): drop or support other fields 217 return nil 218 } 219 switch { 220 case getNum != nil: 221 n := float64(getNum()) 222 if !fcmp(n, fval) { 223 return nil 224 } 225 case getStr != nil: 226 if !scmp(getStr(), sval) { 227 return nil 228 } 229 } 230 } else if ef := cmp.GetTestResultErrorField(); ef != "" { 231 if scmp == nil { 232 return nil 233 } 234 if ef != "exception_type" { 235 return nil 236 } 237 var good bool 238 for _, got := range testResult.Exceptions() { 239 if scmp(got, sval) { 240 good = true 241 break 242 } 243 } 244 if !good { 245 return nil 246 } 247 } else { 248 return nil 249 } 250 251 } 252 want := rule.GetComputedStatus() 253 return &want 254 } 255 256 func evalTargetProperties(rule *evalpb.Rule, targetResult TargetResult) *tspb.TestStatus { 257 for _, cmp := range rule.TestResultComparisons { 258 if cmp.Comparison == nil { 259 return nil 260 } 261 // Only EQ is supported 262 if cmp.Comparison.Op != evalpb.Comparison_OP_EQ { 263 return nil 264 } 265 // Only target_status is supported 266 if f := cmp.GetTargetStatus(); f == true { 267 getSts := targetResult.TargetStatus 268 stscmp := targetStatusEQ 269 stsval := cmp.Comparison.GetTargetStatusValue() 270 if !stscmp(getSts(), stsval) { 271 return nil 272 } 273 } else { 274 return nil 275 } 276 } 277 want := rule.GetComputedStatus() 278 return &want 279 }