pkg/summarizer/analyzers/flipanalyzer.go (72 lines of code) (raw):
/*
Copyright 2020 The TestGrid Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package analyzers represents ways to analyze healthiness and flakiness of tests
package analyzers
import (
summarypb "github.com/GoogleCloudPlatform/testgrid/pb/summary"
"github.com/GoogleCloudPlatform/testgrid/pkg/summarizer/common"
)
const analyzerName = "flipanalyzer"
// FlipAnalyzer implements functions that calculate flakiness as a ratio of failed tests to total tests
type FlipAnalyzer struct {
RelevantStatus map[string][]StatusCategory
}
// StatusCategory is a simiplified status that allows only "Pass", "Fail", and "Flaky"
type StatusCategory int32
// StatusPass, StatusFail, and StatusFlaky are the status categories this analyzer works with.
const (
StatusPass StatusCategory = iota
StatusFail
StatusFlaky
)
// GetFlakiness returns a HealthinessInfo message
func (ea *FlipAnalyzer) GetFlakiness(gridMetrics []*common.GridMetrics, minRuns int, startDate int, endDate int, tab string) *summarypb.HealthinessInfo {
// Delegate to a BaseAnalyzer and change the flakiness scores.
// (And average flakiness)
var ba BaseAnalyzer
healthinessInfo := ba.GetFlakiness(gridMetrics, minRuns, startDate, endDate, tab)
var averageFlakiness float32
for _, test := range healthinessInfo.Tests {
test.Flakiness = calculateFlipFlakiness(ea.RelevantStatus[test.DisplayName])
averageFlakiness += test.Flakiness
}
if len(healthinessInfo.Tests) == 0 {
healthinessInfo.AverageFlakiness = 0
} else {
healthinessInfo.AverageFlakiness = averageFlakiness / float32(len(healthinessInfo.Tests))
}
return healthinessInfo
}
const ignoreFailuresInARow = 3
func consecutiveFailures(statuses []StatusCategory, i int) int {
var result int
for i < len(statuses) && statuses[i] == StatusFail {
result++
i++
}
return result
}
// calculateFlipFlakiness gets a calculation of flakiness based on number of flips to failing rather than number of failures
// statuses should have already filtered to the correct time horizon and removed infra failures
// Returns a percentage between 0 and 100
func calculateFlipFlakiness(statuses []StatusCategory) float32 {
var flips int
var considered int
lastPassing := true // No flakes if we pass 100%
var i int
for i < len(statuses) {
cf := consecutiveFailures(statuses, i)
if cf >= ignoreFailuresInARow {
// Ignore the run of failures
i += cf
if i >= len(statuses) {
break
}
}
s := statuses[i]
considered++
if s == StatusPass {
lastPassing = true
} else if s == StatusFlaky {
// Consider this as always a flip (because there was a flip involved), but it did pass.
flips++
lastPassing = true
} else {
// Failing
if lastPassing {
flips++
}
lastPassing = false
}
i++
}
if considered == 0 {
return 0
}
return 100 * float32(flips) / float32(considered)
}