blob: d438cdb42e88975a712374cb67510ed33ea94a57 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestCalculateBlamelistDistances(t *testing.T) {
const pass = "PASS"
const failure = "FAIL"
// Mapping from builder name to the status of the test in question at each
// commit position.
allBuilds := map[string]map[int]string{
"bar": {
499: pass,
502: pass,
503: pass,
504: failure,
506: failure,
511: failure,
},
"foo": {
500: pass,
505: failure,
510: pass,
515: failure,
},
"allpass": {
499: pass,
502: pass,
511: pass,
},
}
// Mapping from commit (by position) to expected CI builder distances for
// that commit.
expected := map[int]map[string]int{
502: {
"bar": 2,
"foo": 0,
},
503: {
"bar": 1,
"foo": 0,
},
504: {
"bar": 0,
"foo": 0,
},
505: {
"bar": -1,
"foo": 0,
},
}
var results []nearbyTestResult
for builder, builds := range allBuilds {
for position, status := range builds {
results = append(results, nearbyTestResult{
Builder: builder,
CommitPosition: position,
Failed: status == failure,
})
}
}
var suspects []suspectCommit
for pos := range expected {
suspects = append(suspects, suspectCommit{
CommitPosition: pos,
BlamelistDistances: make(map[string]int),
})
}
calculateBlamelistDistances(results, suspects)
got := make(map[int]map[string]int)
for _, suspect := range suspects {
got[suspect.CommitPosition] = suspect.BlamelistDistances
}
if diff := cmp.Diff(expected, got); diff != "" {
t.Errorf("Diff:\n%s", diff)
}
}
func TestScoreBlamelistDistances(t *testing.T) {
tests := []struct {
name string
distances []int
// min/max are the inclusive range of allowed scores for the given CI
// blamelist distances. We don't require equality because the exact
// numbers don't matter that much; we just care that the scores follow a
// certain descending pattern as changes get further and further away
// from the first CI failure.
min int
max int
}{
{
// If a change is in the first-failure blamelist of many builders,
// it should have a very high score.
name: "in the blamelist for many builders",
distances: []int{0, 0, 0, 0, 0},
min: 100,
max: 100,
},
{
// Fewer blamelists should lower the score slightly since there are
// fewer data points.
name: "in the blamelist for two builders",
distances: []int{0, 0},
min: 90,
max: 99,
},
{
// If there's only one data point then the score should be a bit
// lower to reflect lack of confidence.
name: "in the blamelist for one builder",
distances: []int{0},
min: 80,
max: 90,
},
{
// The culprit may have landed *before* the first failure for a few
// builders if the failure mode is flaky. The confidence level
// should be lower but still reasonably high.
name: "in earlier blamelists",
distances: []int{0, 1, 2},
min: 60,
max: 70,
},
{
name: "pretty far away from first failures",
distances: []int{6, 10, 5},
min: 15,
max: 25,
},
{
// A negative distance indicates that the change landed *after* the
// first failed build, which makes the change very unlikely to be
// the culprit unless it was a pre-existing failure mode that was
// made worse by the change.
name: "change landed after first failure",
distances: []int{0, -1},
min: 0,
max: 30,
},
{
name: "no blamelist data",
distances: nil,
min: 0,
max: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
score := scoreBlamelistDistances(test.distances)
if score < test.min || score > test.max {
t.Errorf("scoreBlamelistDistances(%d) = %d is outside range [%d, %d]",
test.distances, score, test.min, test.max,
)
} else {
t.Logf("scoreBlamelistDistances(%d) = %d", test.distances, score)
}
})
}
}