go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/autogardener/blamelist_test.go (about) 1 // Copyright 2022 The Fuchsia Authors. All rights reserved. 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 main 6 7 import ( 8 "testing" 9 10 "github.com/google/go-cmp/cmp" 11 ) 12 13 func TestCalculateBlamelistDistances(t *testing.T) { 14 const pass = "PASS" 15 const failure = "FAIL" 16 17 // Mapping from builder name to the status of the test in question at each 18 // commit position. 19 allBuilds := map[string]map[int]string{ 20 "bar": { 21 499: pass, 22 502: pass, 23 503: pass, 24 504: failure, 25 506: failure, 26 511: failure, 27 }, 28 "foo": { 29 500: pass, 30 505: failure, 31 510: pass, 32 515: failure, 33 }, 34 "allpass": { 35 499: pass, 36 502: pass, 37 511: pass, 38 }, 39 } 40 // Mapping from commit (by position) to expected CI builder distances for 41 // that commit. 42 expected := map[int]map[string]int{ 43 502: { 44 "bar": 2, 45 "foo": 0, 46 }, 47 503: { 48 "bar": 1, 49 "foo": 0, 50 }, 51 504: { 52 "bar": 0, 53 "foo": 0, 54 }, 55 505: { 56 "bar": -1, 57 "foo": 0, 58 }, 59 } 60 61 var results []nearbyTestResult 62 for builder, builds := range allBuilds { 63 for position, status := range builds { 64 results = append(results, nearbyTestResult{ 65 Builder: builder, 66 CommitPosition: position, 67 Failed: status == failure, 68 }) 69 } 70 } 71 72 var suspects []suspectCommit 73 for pos := range expected { 74 suspects = append(suspects, suspectCommit{ 75 CommitPosition: pos, 76 BlamelistDistances: make(map[string]int), 77 }) 78 } 79 80 calculateBlamelistDistances(results, suspects) 81 82 got := make(map[int]map[string]int) 83 for _, suspect := range suspects { 84 got[suspect.CommitPosition] = suspect.BlamelistDistances 85 } 86 87 if diff := cmp.Diff(expected, got); diff != "" { 88 t.Errorf("Diff:\n%s", diff) 89 } 90 } 91 92 func TestScoreBlamelistDistances(t *testing.T) { 93 tests := []struct { 94 name string 95 distances []int 96 // min/max are the inclusive range of allowed scores for the given CI 97 // blamelist distances. We don't require equality because the exact 98 // numbers don't matter that much; we just care that the scores follow a 99 // certain descending pattern as changes get further and further away 100 // from the first CI failure. 101 min int 102 max int 103 }{ 104 { 105 // If a change is in the first-failure blamelist of many builders, 106 // it should have a very high score. 107 name: "in the blamelist for many builders", 108 distances: []int{0, 0, 0, 0, 0}, 109 min: 100, 110 max: 100, 111 }, 112 { 113 // Fewer blamelists should lower the score slightly since there are 114 // fewer data points. 115 name: "in the blamelist for two builders", 116 distances: []int{0, 0}, 117 min: 90, 118 max: 99, 119 }, 120 { 121 // If there's only one data point then the score should be a bit 122 // lower to reflect lack of confidence. 123 name: "in the blamelist for one builder", 124 distances: []int{0}, 125 min: 80, 126 max: 90, 127 }, 128 { 129 // The culprit may have landed *before* the first failure for a few 130 // builders if the failure mode is flaky. The confidence level 131 // should be lower but still reasonably high. 132 name: "in earlier blamelists", 133 distances: []int{0, 1, 2}, 134 min: 60, 135 max: 70, 136 }, 137 { 138 name: "pretty far away from first failures", 139 distances: []int{6, 10, 5}, 140 min: 15, 141 max: 25, 142 }, 143 { 144 // A negative distance indicates that the change landed *after* the 145 // first failed build, which makes the change very unlikely to be 146 // the culprit unless it was a pre-existing failure mode that was 147 // made worse by the change. 148 name: "change landed after first failure", 149 distances: []int{0, -1}, 150 min: 0, 151 max: 30, 152 }, 153 { 154 name: "no blamelist data", 155 distances: nil, 156 min: 0, 157 max: 0, 158 }, 159 } 160 161 for _, test := range tests { 162 t.Run(test.name, func(t *testing.T) { 163 score := scoreBlamelistDistances(test.distances) 164 if score < test.min || score > test.max { 165 t.Errorf("scoreBlamelistDistances(%d) = %d is outside range [%d, %d]", 166 test.distances, score, test.min, test.max, 167 ) 168 } else { 169 t.Logf("scoreBlamelistDistances(%d) = %d", test.distances, score) 170 } 171 }) 172 } 173 }