go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/testresults/test_utils.go (about) 1 // Copyright 2022 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 testresults 16 17 import ( 18 "time" 19 20 pb "go.chromium.org/luci/analysis/proto/v1" 21 ) 22 23 // TestResultBuilder provides methods to build a test result for testing. 24 type TestResultBuilder struct { 25 result TestResult 26 } 27 28 func NewTestResult() TestResultBuilder { 29 d := time.Hour 30 result := TestResult{ 31 Project: "proj", 32 TestID: "test_id", 33 PartitionTime: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC), 34 VariantHash: "hash", 35 IngestedInvocationID: "inv-id", 36 RunIndex: 2, 37 ResultIndex: 3, 38 IsUnexpected: true, 39 RunDuration: &d, 40 Status: pb.TestResultStatus_PASS, 41 ExonerationReasons: nil, 42 SubRealm: "realm", 43 Sources: Sources{ 44 RefHash: []byte{0, 1, 2, 3, 4, 5, 6, 7}, 45 Position: 999444, 46 Changelists: []Changelist{ 47 { 48 Host: "mygerrit-review.googlesource.com", 49 Change: 12345678, 50 Patchset: 9, 51 }, 52 { 53 Host: "anothergerrit.gerrit.instance", 54 Change: 234568790, 55 Patchset: 1, 56 }, 57 }, 58 IsDirty: true, 59 }, 60 } 61 return TestResultBuilder{ 62 result: result, 63 } 64 } 65 66 func (b TestResultBuilder) WithProject(project string) TestResultBuilder { 67 b.result.Project = project 68 return b 69 } 70 71 func (b TestResultBuilder) WithTestID(testID string) TestResultBuilder { 72 b.result.TestID = testID 73 return b 74 } 75 76 func (b TestResultBuilder) WithPartitionTime(partitionTime time.Time) TestResultBuilder { 77 b.result.PartitionTime = partitionTime 78 return b 79 } 80 81 func (b TestResultBuilder) WithVariantHash(variantHash string) TestResultBuilder { 82 b.result.VariantHash = variantHash 83 return b 84 } 85 86 func (b TestResultBuilder) WithIngestedInvocationID(invID string) TestResultBuilder { 87 b.result.IngestedInvocationID = invID 88 return b 89 } 90 91 func (b TestResultBuilder) WithRunIndex(runIndex int64) TestResultBuilder { 92 b.result.RunIndex = runIndex 93 return b 94 } 95 96 func (b TestResultBuilder) WithResultIndex(resultIndex int64) TestResultBuilder { 97 b.result.ResultIndex = resultIndex 98 return b 99 } 100 101 func (b TestResultBuilder) WithIsUnexpected(unexpected bool) TestResultBuilder { 102 b.result.IsUnexpected = unexpected 103 return b 104 } 105 106 func (b TestResultBuilder) WithRunDuration(duration time.Duration) TestResultBuilder { 107 b.result.RunDuration = &duration 108 return b 109 } 110 111 func (b TestResultBuilder) WithoutRunDuration() TestResultBuilder { 112 b.result.RunDuration = nil 113 return b 114 } 115 116 func (b TestResultBuilder) WithStatus(status pb.TestResultStatus) TestResultBuilder { 117 b.result.Status = status 118 return b 119 } 120 121 func (b TestResultBuilder) WithExonerationReasons(exonerationReasons ...pb.ExonerationReason) TestResultBuilder { 122 b.result.ExonerationReasons = exonerationReasons 123 return b 124 } 125 126 func (b TestResultBuilder) WithoutExoneration() TestResultBuilder { 127 b.result.ExonerationReasons = nil 128 return b 129 } 130 131 func (b TestResultBuilder) WithSubRealm(subRealm string) TestResultBuilder { 132 b.result.SubRealm = subRealm 133 return b 134 } 135 136 func (b TestResultBuilder) WithSources(sources Sources) TestResultBuilder { 137 // Copy sources to avoid aliasing artifacts. Changes made to 138 // slices within the sources struct after this call should 139 // not propagate to the test result. 140 b.result.Sources = copySources(sources) 141 return b 142 } 143 144 func (b TestResultBuilder) WithIsFromBisection(value bool) TestResultBuilder { 145 b.result.IsFromBisection = value 146 return b 147 } 148 149 func (b TestResultBuilder) Build() *TestResult { 150 // Copy the result, so that calling further methods on the builder does 151 // not change the returned test verdict. 152 result := new(TestResult) 153 *result = b.result 154 result.Sources = copySources(b.result.Sources) 155 return result 156 } 157 158 // copySources makes a deep copy of the given code sources. 159 func copySources(sources Sources) Sources { 160 var refHash []byte 161 if sources.RefHash != nil { 162 refHash = make([]byte, len(sources.RefHash)) 163 copy(refHash, sources.RefHash) 164 } 165 166 cls := make([]Changelist, len(sources.Changelists)) 167 copy(cls, sources.Changelists) 168 169 return Sources{ 170 RefHash: refHash, 171 Position: sources.Position, 172 Changelists: cls, 173 IsDirty: sources.IsDirty, 174 } 175 } 176 177 // TestVerdictBuilder provides methods to build a test variant for testing. 178 type TestVerdictBuilder struct { 179 baseResult TestResult 180 status *pb.TestVerdictStatus 181 runStatuses []RunStatus 182 passedAvgDuration *time.Duration 183 } 184 185 type RunStatus int64 186 187 const ( 188 Unexpected RunStatus = iota 189 Flaky 190 Expected 191 ) 192 193 func NewTestVerdict() *TestVerdictBuilder { 194 result := new(TestVerdictBuilder) 195 result.baseResult = *NewTestResult().WithStatus(pb.TestResultStatus_PASS).Build() 196 status := pb.TestVerdictStatus_FLAKY 197 result.status = &status 198 result.runStatuses = nil 199 d := 919191 * time.Microsecond 200 result.passedAvgDuration = &d 201 return result 202 } 203 204 // WithBaseTestResult specifies a test result to use as the template for 205 // the test variant's test results. 206 func (b *TestVerdictBuilder) WithBaseTestResult(testResult *TestResult) *TestVerdictBuilder { 207 b.baseResult = *testResult 208 return b 209 } 210 211 // WithPassedAvgDuration specifies the average duration to use for 212 // passed test results. If setting to a non-nil value, make sure 213 // to set the result status as passed on the base test result if 214 // using this option. 215 func (b *TestVerdictBuilder) WithPassedAvgDuration(duration *time.Duration) *TestVerdictBuilder { 216 b.passedAvgDuration = duration 217 return b 218 } 219 220 // WithStatus specifies the status of the test verdict. 221 func (b *TestVerdictBuilder) WithStatus(status pb.TestVerdictStatus) *TestVerdictBuilder { 222 b.status = &status 223 return b 224 } 225 226 // WithRunStatus specifies the status of runs of the test verdict. 227 func (b *TestVerdictBuilder) WithRunStatus(runStatuses ...RunStatus) *TestVerdictBuilder { 228 b.runStatuses = runStatuses 229 return b 230 } 231 232 func applyStatus(trs []*TestResult, status pb.TestVerdictStatus) { 233 // Set all test results to unexpected, not exonerated by default. 234 for _, tr := range trs { 235 tr.IsUnexpected = true 236 tr.ExonerationReasons = nil 237 } 238 switch status { 239 case pb.TestVerdictStatus_EXONERATED: 240 for _, tr := range trs { 241 tr.ExonerationReasons = []pb.ExonerationReason{pb.ExonerationReason_OCCURS_ON_MAINLINE} 242 } 243 case pb.TestVerdictStatus_UNEXPECTED: 244 // No changes required. 245 case pb.TestVerdictStatus_EXPECTED: 246 allSkipped := true 247 for _, tr := range trs { 248 tr.IsUnexpected = false 249 if tr.Status != pb.TestResultStatus_SKIP { 250 allSkipped = false 251 } 252 } 253 // Make sure not all test results are SKIPPED, to avoid the status 254 // UNEXPECTEDLY_SKIPPED. 255 if allSkipped { 256 trs[0].Status = pb.TestResultStatus_CRASH 257 } 258 case pb.TestVerdictStatus_UNEXPECTEDLY_SKIPPED: 259 for _, tr := range trs { 260 tr.Status = pb.TestResultStatus_SKIP 261 } 262 case pb.TestVerdictStatus_FLAKY: 263 trs[0].IsUnexpected = false 264 default: 265 panic("status must be specified") 266 } 267 } 268 269 // applyRunStatus applies the given run status to the given test results. 270 func applyRunStatus(trs []*TestResult, runStatus RunStatus) { 271 for _, tr := range trs { 272 tr.IsUnexpected = true 273 } 274 switch runStatus { 275 case Expected: 276 for _, tr := range trs { 277 tr.IsUnexpected = false 278 } 279 case Flaky: 280 trs[0].IsUnexpected = false 281 case Unexpected: 282 // All test results already unexpected. 283 } 284 } 285 286 func applyAvgPassedDuration(trs []*TestResult, passedAvgDuration *time.Duration) { 287 if passedAvgDuration == nil { 288 for _, tr := range trs { 289 if tr.Status == pb.TestResultStatus_PASS { 290 tr.RunDuration = nil 291 } 292 } 293 return 294 } 295 296 passCount := 0 297 for _, tr := range trs { 298 if tr.Status == pb.TestResultStatus_PASS { 299 passCount++ 300 } 301 } 302 passIndex := 0 303 for _, tr := range trs { 304 if tr.Status == pb.TestResultStatus_PASS { 305 d := *passedAvgDuration 306 if passCount == 1 { 307 // If there is only one pass, assign it the 308 // set duration. 309 tr.RunDuration = &d 310 break 311 } 312 if passIndex == 0 && passCount%2 == 1 { 313 // If there are an odd number of passes, and 314 // more than one pass, assign the first pass 315 // a nil duration. 316 tr.RunDuration = nil 317 } else { 318 // Assigning alternating passes 2*d the duration 319 // and 0 duration, to keep the average correct. 320 if passIndex%2 == 0 { 321 d = d * 2 322 tr.RunDuration = &d 323 } else { 324 d = 0 325 tr.RunDuration = &d 326 } 327 } 328 passIndex++ 329 } 330 } 331 } 332 333 func (b *TestVerdictBuilder) Build() []*TestResult { 334 runs := 2 335 if len(b.runStatuses) > 0 { 336 runs = len(b.runStatuses) 337 } 338 339 // Create two test results per run, to allow 340 // for all expected, all unexpected and 341 // flaky (mixed expected+unexpected) statuses 342 // to be represented. 343 trs := make([]*TestResult, 0, runs*2) 344 for i := 0; i < runs*2; i++ { 345 tr := new(TestResult) 346 *tr = b.baseResult 347 tr.RunIndex = int64(i / 2) 348 tr.ResultIndex = int64(i % 2) 349 trs = append(trs, tr) 350 } 351 352 // Normally only one of these should be set. 353 // If both are set, run statuses has precedence. 354 if b.status != nil { 355 applyStatus(trs, *b.status) 356 } 357 for i, runStatus := range b.runStatuses { 358 runTRs := trs[i*2 : (i+1)*2] 359 applyRunStatus(runTRs, runStatus) 360 } 361 362 applyAvgPassedDuration(trs, b.passedAvgDuration) 363 return trs 364 }