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  }