go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/changepoints/bayesian/bayesian_test.go (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package bayesian 16 17 import ( 18 "testing" 19 20 "go.chromium.org/luci/analysis/internal/changepoints/inputbuffer" 21 22 . "github.com/smartystreets/goconvey/convey" 23 ) 24 25 func TestBayesianAnalysis(t *testing.T) { 26 a := ChangepointPredictor{ 27 ChangepointLikelihood: 0.01, 28 HasUnexpectedPrior: BetaDistribution{ 29 Alpha: 0.3, 30 Beta: 0.5, 31 }, 32 UnexpectedAfterRetryPrior: BetaDistribution{ 33 Alpha: 0.5, 34 Beta: 0.5, 35 }, 36 } 37 Convey("Pass to fail transition 1", t, func() { 38 var ( 39 positions = []int{1, 2, 3, 4, 5, 6} 40 total = []int{2, 2, 1, 1, 2, 2} 41 hasUnexpected = []int{0, 0, 0, 1, 2, 2} 42 ) 43 vs := inputbuffer.Verdicts(positions, total, hasUnexpected) 44 changePoints := a.identifyChangePoints(vs) 45 So(changePoints, ShouldResemble, []int{3}) 46 }) 47 48 Convey("Pass to fail transition 2", t, func() { 49 var ( 50 positions = []int{1, 2, 3, 4, 5, 6} 51 total = []int{2, 2, 1, 1, 2, 2} 52 hasUnexpected = []int{0, 0, 1, 1, 2, 2} 53 ) 54 vs := inputbuffer.Verdicts(positions, total, hasUnexpected) 55 changePoints := a.identifyChangePoints(vs) 56 So(changePoints, ShouldResemble, []int{2}) 57 }) 58 59 Convey("Pass to flake transition", t, func() { 60 var ( 61 positions = []int{1, 1, 2, 2, 2, 2} 62 total = []int{3, 3, 1, 2, 3, 3} 63 hasUnexpected = []int{0, 0, 0, 2, 3, 3} 64 ) 65 vs := inputbuffer.Verdicts(positions, total, hasUnexpected) 66 changePoints := a.identifyChangePoints(vs) 67 So(changePoints, ShouldResemble, []int{2}) 68 }) 69 70 Convey("Pass to fail to pass transition", t, func() { 71 var ( 72 positions = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14} 73 total = []int{2, 2, 3, 2, 3, 1, 1, 2, 2, 3, 2, 3, 2, 2} 74 hasUnexpected = []int{0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 2, 3, 0, 0} 75 ) 76 vs := inputbuffer.Verdicts(positions, total, hasUnexpected) 77 changePoints := a.identifyChangePoints(vs) 78 So(changePoints, ShouldResemble, []int{5, 12}) 79 }) 80 81 Convey("Pass to flake transition", t, func() { 82 var ( 83 positions = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 84 total = []int{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2} 85 hasUnexpected = []int{0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} 86 ) 87 vs := inputbuffer.Verdicts(positions, total, hasUnexpected) 88 changePoints := a.identifyChangePoints(vs) 89 So(changePoints, ShouldResemble, []int{6}) 90 }) 91 92 Convey("Flake to fail transition", t, func() { 93 var ( 94 positions = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 95 total = []int{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2} 96 hasUnexpected = []int{1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2} 97 ) 98 vs := inputbuffer.Verdicts(positions, total, hasUnexpected) 99 changePoints := a.identifyChangePoints(vs) 100 So(changePoints, ShouldResemble, []int{6}) 101 }) 102 103 Convey("Pass consistently", t, func() { 104 var ( 105 positions = []int{1, 2, 3, 4, 5, 6, 7, 8} 106 total = []int{2, 2, 2, 2, 2, 2, 2, 2} 107 hasUnexpected = []int{0, 0, 0, 0, 0, 0, 0, 0} 108 ) 109 vs := inputbuffer.Verdicts(positions, total, hasUnexpected) 110 changePoints := a.identifyChangePoints(vs) 111 So(len(changePoints), ShouldEqual, 0) 112 }) 113 114 Convey("Fail consistently", t, func() { 115 var ( 116 positions = []int{1, 2, 3, 4, 5, 6, 7, 8} 117 total = []int{2, 2, 2, 2, 2, 2, 2, 2} 118 hasUnexpected = []int{2, 2, 2, 2, 2, 2, 2, 2} 119 ) 120 vs := inputbuffer.Verdicts(positions, total, hasUnexpected) 121 changePoints := a.identifyChangePoints(vs) 122 So(len(changePoints), ShouldEqual, 0) 123 }) 124 125 Convey("Flake", t, func() { 126 var ( 127 positions = []int{1, 2, 3, 4, 5, 6, 7, 8} 128 total = []int{2, 2, 2, 2, 2, 2, 2, 2} 129 hasUnexpected = []int{1, 0, 1, 0, 0, 1, 0, 2} 130 ) 131 vs := inputbuffer.Verdicts(positions, total, hasUnexpected) 132 changePoints := a.identifyChangePoints(vs) 133 So(len(changePoints), ShouldEqual, 0) 134 }) 135 136 Convey("(Fail, Pass after retry) to (Fail, Fail after retry)", t, func() { 137 var ( 138 positions = []int{1, 2, 3, 4, 5, 6, 7, 8} 139 total = []int{2, 2, 2, 2, 2, 2, 2, 2} 140 hasUnexpected = []int{2, 2, 2, 2, 2, 2, 2, 2} 141 retries = []int{2, 2, 2, 2, 2, 2, 2, 2} 142 unexpectedAfterRetry = []int{0, 0, 0, 0, 2, 2, 2, 2} 143 ) 144 vs := inputbuffer.VerdictsWithRetries(positions, total, hasUnexpected, retries, unexpectedAfterRetry) 145 changePoints := a.identifyChangePoints(vs) 146 So(changePoints, ShouldResemble, []int{4}) 147 }) 148 149 Convey("(Fail, Fail after retry) consistently", t, func() { 150 var ( 151 positions = []int{1, 2, 3, 4, 5, 6, 7, 8} 152 total = []int{2, 2, 2, 2, 2, 2, 2, 2} 153 hasUnexpected = []int{2, 2, 2, 2, 2, 2, 2, 2} 154 retries = []int{2, 2, 2, 2, 2, 2, 2, 2} 155 unexpectedAfterRetry = []int{2, 2, 2, 2, 2, 2, 2, 2} 156 ) 157 vs := inputbuffer.VerdictsWithRetries(positions, total, hasUnexpected, retries, unexpectedAfterRetry) 158 changePoints := a.identifyChangePoints(vs) 159 So(len(changePoints), ShouldEqual, 0) 160 }) 161 162 Convey("(Fail, Fail after retry) to (Fail, Flaky on retry)", t, func() { 163 var ( 164 // The change point should be detected between commit positions 3 and 5. 165 positions = []int{1, 2, 3, 5, 5, 5, 7, 7} 166 total = []int{3, 3, 3, 1, 3, 3, 3, 3} 167 hasUnexpected = []int{3, 3, 3, 1, 3, 3, 3, 3} 168 retries = []int{3, 3, 3, 1, 3, 3, 3, 3} 169 unexpectedAfterRetry = []int{3, 3, 3, 1, 0, 0, 1, 1} 170 ) 171 vs := inputbuffer.VerdictsWithRetries(positions, total, hasUnexpected, retries, unexpectedAfterRetry) 172 changePoints := a.identifyChangePoints(vs) 173 So(changePoints, ShouldResemble, []int{3}) 174 }) 175 } 176 177 // Output as of March 2023 on Intel Skylake CPU @ 2.00GHz: 178 // BenchmarkBayesianAnalysisConsistentPass-48 30054 39879 ns/op 18 B/op 0 allocs/op 179 func BenchmarkBayesianAnalysisConsistentPass(b *testing.B) { 180 a := ChangepointPredictor{ 181 ChangepointLikelihood: 0.01, 182 HasUnexpectedPrior: BetaDistribution{ 183 Alpha: 0.3, 184 Beta: 0.5, 185 }, 186 UnexpectedAfterRetryPrior: BetaDistribution{ 187 Alpha: 0.5, 188 Beta: 0.5, 189 }, 190 } 191 192 var vs []inputbuffer.PositionVerdict 193 194 // Consistently passing test. This represents ~99% of tests. 195 for i := 0; i < 2000; i++ { 196 vs = append(vs, inputbuffer.PositionVerdict{ 197 CommitPosition: i, 198 IsSimpleExpectedPass: true, 199 }) 200 } 201 for i := 0; i < b.N; i++ { 202 result := a.identifyChangePoints(vs) 203 if len(result) != 0 { 204 panic("unexpected result") 205 } 206 } 207 } 208 209 // Output as of March 2023 on Intel Skylake CPU @ 2.00GHz: 210 // BenchmarkBayesianAnalysisFlaky-48 1500 796446 ns/op 396 B/op 0 allocs/op 211 func BenchmarkBayesianAnalysisFlaky(b *testing.B) { 212 a := ChangepointPredictor{ 213 ChangepointLikelihood: 0.01, 214 HasUnexpectedPrior: BetaDistribution{ 215 Alpha: 0.3, 216 Beta: 0.5, 217 }, 218 UnexpectedAfterRetryPrior: BetaDistribution{ 219 Alpha: 0.5, 220 Beta: 0.5, 221 }, 222 } 223 // Flaky test. 224 var vs []inputbuffer.PositionVerdict 225 for i := 0; i < 2000; i++ { 226 if i%2 == 0 { 227 vs = append(vs, inputbuffer.PositionVerdict{ 228 CommitPosition: i, 229 IsSimpleExpectedPass: true, 230 }) 231 } else { 232 vs = append(vs, inputbuffer.PositionVerdict{ 233 CommitPosition: i, 234 Details: inputbuffer.VerdictDetails{ 235 Runs: []inputbuffer.Run{ 236 { 237 Expected: inputbuffer.ResultCounts{ 238 PassCount: 1, 239 }, 240 Unexpected: inputbuffer.ResultCounts{ 241 FailCount: 1, 242 }, 243 }, 244 }, 245 }, 246 }) 247 } 248 } 249 for i := 0; i < b.N; i++ { 250 result := a.identifyChangePoints(vs) 251 if len(result) != 0 { 252 panic("unexpected result") 253 } 254 } 255 }