go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/testresults/stability/test_data.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 stability 16 17 import ( 18 "context" 19 "time" 20 21 "go.chromium.org/luci/server/span" 22 23 "go.chromium.org/luci/analysis/internal/testresults" 24 "go.chromium.org/luci/analysis/pbutil" 25 pb "go.chromium.org/luci/analysis/proto/v1" 26 ) 27 28 var referenceTime = time.Date(2022, time.June, 17, 8, 0, 0, 0, time.UTC) 29 30 // CreateQueryStabilityTestData creates test data in Spanner for testing 31 // QueryStability. 32 func CreateQueryStabilityTestData(ctx context.Context) error { 33 var1 := pbutil.Variant("key1", "val1", "key2", "val1") 34 var2 := pbutil.Variant("key1", "val2", "key2", "val1") 35 var3 := pbutil.Variant("key1", "val2", "key2", "val2") 36 37 onBranch := pbutil.SourceRefHash(&pb.SourceRef{ 38 System: &pb.SourceRef_Gitiles{ 39 Gitiles: &pb.GitilesRef{ 40 Host: "mysources.googlesource.com", 41 Project: "myproject/src", 42 Ref: "refs/heads/mybranch", 43 }, 44 }, 45 }) 46 otherBranch := pbutil.SourceRefHash(&pb.SourceRef{ 47 System: &pb.SourceRef_Gitiles{ 48 Gitiles: &pb.GitilesRef{ 49 Host: "mysources.googlesource.com", 50 Project: "myproject/src", 51 Ref: "refs/heads/otherbranch", 52 }, 53 }, 54 }) 55 56 type verdict struct { 57 partitionTime time.Time 58 variant *pb.Variant 59 invocationID string 60 runStatuses []testresults.RunStatus 61 sources testresults.Sources 62 } 63 64 changelists := func(clOwnerKind pb.ChangelistOwnerKind, numbers ...int64) []testresults.Changelist { 65 var changelists []testresults.Changelist 66 for _, clNum := range numbers { 67 changelists = append(changelists, testresults.Changelist{ 68 Host: "mygerrit-review.googlesource.com", 69 Change: clNum, 70 Patchset: 5, 71 OwnerKind: clOwnerKind, 72 }) 73 } 74 return changelists 75 } 76 77 _, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 78 // pass, fail is shorthand here for expected and unexpected run, 79 // where for the purposes of this RPC, a flaky run counts as 80 // "expected" (as it has at least one expected result). 81 failPass := []testresults.RunStatus{testresults.Unexpected, testresults.Flaky} 82 failPassPass := []testresults.RunStatus{testresults.Unexpected, testresults.Flaky, testresults.Flaky} 83 pass := []testresults.RunStatus{testresults.Flaky} 84 fail := []testresults.RunStatus{testresults.Unexpected} 85 failFail := []testresults.RunStatus{testresults.Unexpected, testresults.Unexpected} 86 87 day := 24 * time.Hour 88 89 automationOwner := pb.ChangelistOwnerKind_AUTOMATION 90 humanOwner := pb.ChangelistOwnerKind_HUMAN 91 unspecifiedOwner := pb.ChangelistOwnerKind_CHANGELIST_OWNER_UNSPECIFIED 92 93 verdicts := []verdict{ 94 { 95 partitionTime: referenceTime.Add(-14*day - time.Microsecond), 96 variant: var1, 97 // Partition time too early, so should be ignored. 98 invocationID: "sourceverdict0-ignore-partitiontime", 99 runStatuses: failPass, 100 sources: testresults.Sources{ 101 RefHash: onBranch, 102 Position: 1, 103 }, 104 }, 105 { 106 partitionTime: referenceTime.Add(-13*day - 1*time.Hour), 107 variant: var1, 108 invocationID: "sourceverdict1", 109 runStatuses: failPass, 110 sources: testresults.Sources{ 111 RefHash: onBranch, 112 Position: 90, 113 }, 114 }, 115 { 116 partitionTime: referenceTime.Add(-12 * day), 117 variant: var1, 118 invocationID: "sourceverdict2", 119 runStatuses: failPass, 120 sources: testresults.Sources{ 121 RefHash: onBranch, 122 Position: 100, 123 }, 124 }, 125 { 126 partitionTime: referenceTime.Add(-10 * day), 127 variant: var1, 128 invocationID: "sourceverdict3-part1", 129 runStatuses: fail, 130 sources: testresults.Sources{ 131 RefHash: onBranch, 132 Position: 100, 133 Changelists: changelists(humanOwner, 1), 134 }, 135 }, 136 { 137 partitionTime: referenceTime.Add(-9 * day), 138 variant: var1, 139 // Should combine with sourceverdict3-part1 to produce a run-flaky verdict, as it 140 // has the same sources under test. 141 invocationID: "sourceverdict3-part2", 142 runStatuses: pass, 143 sources: testresults.Sources{ 144 RefHash: onBranch, 145 Position: 100, 146 Changelists: changelists(humanOwner, 1), 147 }, 148 }, 149 { 150 partitionTime: referenceTime.Add(-8*day - 3*time.Hour), 151 variant: var1, 152 // source verdict 3 should be used preferentially as it is flaky 153 // and tests the same CL. 154 invocationID: "sourceverdict4-ignore", 155 runStatuses: pass, 156 sources: testresults.Sources{ 157 RefHash: onBranch, 158 Position: 120, 159 Changelists: changelists(humanOwner, 1), 160 }, 161 }, 162 { 163 partitionTime: referenceTime.Add(-8*day - 2*time.Hour), 164 variant: var1, 165 invocationID: "sourceverdict5", 166 runStatuses: pass, 167 sources: testresults.Sources{ 168 RefHash: onBranch, 169 Position: 120, 170 Changelists: changelists(unspecifiedOwner, 2), 171 }, 172 }, 173 { 174 partitionTime: referenceTime.Add(-8*day - time.Hour), 175 variant: var1, 176 invocationID: "sourceverdict6", 177 runStatuses: failPass, 178 sources: testresults.Sources{ 179 RefHash: onBranch, 180 Position: 120, 181 IsDirty: true, 182 }, 183 }, 184 { 185 partitionTime: referenceTime.Add(-8 * day), 186 variant: var1, 187 // Should be distinct from source verdict 5 because both have 188 // IsDirty set, so we cannot confirm the soruces are identical. 189 invocationID: "sourceverdict7", 190 runStatuses: failPassPass, 191 sources: testresults.Sources{ 192 RefHash: onBranch, 193 Position: 120, 194 IsDirty: true, 195 }, 196 }, 197 { 198 partitionTime: referenceTime.Add(-8 * day), 199 variant: var1, 200 // Automation-authored, so should be ignored. 201 invocationID: "sourceverdict8-ignore", 202 runStatuses: failPass, 203 sources: testresults.Sources{ 204 RefHash: onBranch, 205 Position: 120, 206 Changelists: changelists(automationOwner, 4), 207 }, 208 }, 209 { 210 partitionTime: referenceTime.Add(-7 * day), 211 variant: var1, 212 invocationID: "sourceverdict9-part1", 213 runStatuses: failFail, 214 sources: testresults.Sources{ 215 RefHash: onBranch, 216 Position: 128, 217 }, 218 }, 219 { 220 partitionTime: referenceTime.Add(-7 * day), 221 variant: var1, 222 // Should merge with sourceverdict8-part1 due to sharing 223 // same sources. 224 invocationID: "sourceverdict9-part2", 225 runStatuses: pass, 226 sources: testresults.Sources{ 227 RefHash: onBranch, 228 Position: 128, 229 }, 230 }, 231 { 232 partitionTime: referenceTime.Add(-7 * day), 233 variant: var2, 234 // Different variant, so should be ignored. 235 invocationID: "sourceverdict10-ignore-var2", 236 runStatuses: failPass, 237 sources: testresults.Sources{ 238 RefHash: onBranch, 239 Position: 130, 240 }, 241 }, 242 { 243 partitionTime: referenceTime.Add(-7 * day), 244 variant: var3, 245 // For variant 3. 246 invocationID: "sourceverdict11", 247 runStatuses: failPass, 248 sources: testresults.Sources{ 249 RefHash: otherBranch, 250 Position: 130, 251 }, 252 }, 253 { 254 partitionTime: referenceTime.Add(-7 * day), 255 variant: var1, 256 // Other branch, so should be ignored. 257 invocationID: "sourceverdict12-ignore-offbranch", 258 runStatuses: failPass, 259 sources: testresults.Sources{ 260 RefHash: otherBranch, 261 Position: 130, 262 }, 263 }, 264 { 265 partitionTime: referenceTime.Add(-7 * day), 266 variant: var1, 267 invocationID: "sourceverdict13-ignore-no-sources", 268 runStatuses: pass, 269 sources: testresults.Sources{ 270 // Has no branch and position information, so should be ignored. 271 Changelists: changelists(pb.ChangelistOwnerKind_HUMAN, 10), 272 }, 273 }, 274 { 275 partitionTime: referenceTime.Add(-7 * day), 276 variant: var1, 277 // A result for the same CL as being queried, so should be ignored 278 // to avoid CLs contributing to their own exoneration. 279 invocationID: "sourceverdict14-ignore-same-cl-as-query", 280 runStatuses: failPass, 281 sources: testresults.Sources{ 282 RefHash: otherBranch, 283 Position: 130, 284 Changelists: changelists(pb.ChangelistOwnerKind_HUMAN, 888888), 285 }, 286 }, 287 { 288 partitionTime: referenceTime.Add(-6 * day), 289 variant: var1, 290 invocationID: "sourceverdict15", 291 runStatuses: pass, 292 sources: testresults.Sources{ 293 RefHash: onBranch, 294 Position: 129, 295 }, 296 }, 297 // Begin consecutive failure streak. 298 { 299 partitionTime: referenceTime.Add(-6 * day), 300 variant: var1, 301 invocationID: "sourceverdict16-part1", 302 runStatuses: failFail, 303 sources: testresults.Sources{ 304 RefHash: onBranch, 305 Position: 130, 306 }, 307 }, 308 { 309 partitionTime: referenceTime.Add(-6 * day), 310 variant: var1, 311 // Should merge with sourceverdict13-part1 due to sharing 312 // same sources. 313 invocationID: "sourceverdict16-part2", 314 runStatuses: failFail, 315 sources: testresults.Sources{ 316 RefHash: onBranch, 317 Position: 130, 318 }, 319 }, 320 { 321 partitionTime: referenceTime.Add(-1 * day), 322 variant: var1, 323 invocationID: "sourceverdict17", 324 // Only one run should be used as the verdict relates to presubmit testing. 325 runStatuses: failPass, 326 sources: testresults.Sources{ 327 RefHash: onBranch, 328 Changelists: changelists(pb.ChangelistOwnerKind_HUMAN, 888777), 329 Position: 140, 330 }, 331 }, 332 } 333 334 for _, v := range verdicts { 335 baseTestResult := testresults.NewTestResult(). 336 WithProject("project"). 337 WithTestID("test_id"). 338 WithVariantHash(pbutil.VariantHash(v.variant)). 339 WithPartitionTime(v.partitionTime). 340 WithIngestedInvocationID(v.invocationID). 341 WithSubRealm("realm"). 342 WithStatus(pb.TestResultStatus_PASS). 343 WithSources(v.sources) 344 345 trs := testresults.NewTestVerdict(). 346 WithBaseTestResult(baseTestResult.Build()). 347 WithRunStatus(v.runStatuses...). 348 Build() 349 for _, tr := range trs { 350 span.BufferWrite(ctx, tr.SaveUnverified()) 351 } 352 } 353 354 return nil 355 }) 356 return err 357 } 358 359 func QueryStabilitySampleRequest() QueryStabilityOptions { 360 var1 := pbutil.Variant("key1", "val1", "key2", "val1") 361 var3 := pbutil.Variant("key1", "val2", "key2", "val2") 362 testVariants := []*pb.QueryTestVariantStabilityRequest_TestVariantPosition{ 363 { 364 TestId: "test_id", 365 Variant: var1, 366 Sources: &pb.Sources{ 367 GitilesCommit: &pb.GitilesCommit{ 368 Host: "mysources.googlesource.com", 369 Project: "myproject/src", 370 Ref: "refs/heads/mybranch", 371 CommitHash: "aabbccddeeff00112233aabbccddeeff00112233", 372 Position: 130, 373 }, 374 Changelists: []*pb.GerritChange{ 375 { 376 Host: "mygerrit-review.googlesource.com", 377 Project: "mygerrit/src", 378 Change: 888888, 379 Patchset: 1, 380 }, 381 }, 382 }, 383 }, 384 { 385 TestId: "test_id", 386 Variant: var3, 387 Sources: &pb.Sources{ 388 GitilesCommit: &pb.GitilesCommit{ 389 Host: "mysources.googlesource.com", 390 Project: "myproject/src", 391 Ref: "refs/heads/otherbranch", 392 CommitHash: "00bbccddeeff00112233aabbccddeeff00112233", 393 Position: 120, 394 }, 395 }, 396 }, 397 } 398 return QueryStabilityOptions{ 399 Project: "project", 400 SubRealms: []string{"realm"}, 401 TestVariantPositions: testVariants, 402 Criteria: CreateSampleStabilityCriteria(), 403 AsAtTime: time.Date(2022, time.June, 17, 8, 0, 0, 0, time.UTC), 404 } 405 } 406 407 func CreateSampleStabilityCriteria() *pb.TestStabilityCriteria { 408 return &pb.TestStabilityCriteria{ 409 FailureRate: &pb.TestStabilityCriteria_FailureRateCriteria{ 410 FailureThreshold: 7, 411 ConsecutiveFailureThreshold: 3, 412 }, 413 FlakeRate: &pb.TestStabilityCriteria_FlakeRateCriteria{ 414 // Use 5 instead of a more typical value like 100 because of the 415 // limited sample data. 416 MinWindow: 5, 417 FlakeThreshold: 2, 418 FlakeRateThreshold: 0.01, 419 }, 420 } 421 } 422 423 // QueryStabilitySampleResponse returns expected response data from QueryFailureRate 424 // after being invoked with QueryFailureRateSampleRequest. 425 // It is assumed test data was setup with CreateQueryFailureRateTestData. 426 func QueryStabilitySampleResponse() []*pb.TestVariantStabilityAnalysis { 427 var1 := pbutil.Variant("key1", "val1", "key2", "val1") 428 var3 := pbutil.Variant("key1", "val2", "key2", "val2") 429 430 analysis := []*pb.TestVariantStabilityAnalysis{ 431 { 432 TestId: "test_id", 433 Variant: var1, 434 FailureRate: &pb.TestVariantStabilityAnalysis_FailureRate{ 435 IsMet: true, 436 UnexpectedTestRuns: 8, 437 ConsecutiveUnexpectedTestRuns: 5, 438 RecentVerdicts: []*pb.TestVariantStabilityAnalysis_FailureRate_RecentVerdict{ 439 { 440 Position: 140, 441 Changelists: []*pb.Changelist{ 442 { 443 Host: "mygerrit-review.googlesource.com", 444 Change: 888777, 445 Patchset: 5, 446 OwnerKind: pb.ChangelistOwnerKind_HUMAN, 447 }, 448 }, 449 Invocations: []string{"sourceverdict17"}, 450 // Unexpected + expected run flattened down to only an unexpected run due to 451 // presubmit results only being allowed to contribute 1 test run. 452 UnexpectedRuns: 1, 453 TotalRuns: 1, 454 }, 455 { 456 Position: 130, // Query position. 457 Invocations: []string{"sourceverdict16-part1", "sourceverdict16-part2"}, 458 UnexpectedRuns: 4, 459 TotalRuns: 4, 460 }, 461 { 462 Position: 129, 463 Invocations: []string{"sourceverdict15"}, 464 TotalRuns: 1, 465 }, 466 { 467 Position: 128, 468 Invocations: []string{"sourceverdict9-part1", "sourceverdict9-part2"}, 469 UnexpectedRuns: 2, 470 TotalRuns: 3, 471 }, 472 { 473 Position: 120, 474 Invocations: []string{"sourceverdict7"}, 475 UnexpectedRuns: 1, 476 TotalRuns: 2, // Verdict truncated to 2 runs to keep total runs on or before query position <= 10. 477 }, 478 }, 479 }, 480 FlakeRate: &pb.TestVariantStabilityAnalysis_FlakeRate{ 481 IsMet: true, 482 TotalVerdicts: 9, 483 RunFlakyVerdicts: 6, 484 FlakeExamples: []*pb.TestVariantStabilityAnalysis_FlakeRate_VerdictExample{ 485 { 486 Position: 140, 487 Changelists: []*pb.Changelist{ 488 { 489 Host: "mygerrit-review.googlesource.com", 490 Change: 888777, 491 Patchset: 5, 492 OwnerKind: pb.ChangelistOwnerKind_HUMAN, 493 }, 494 }, 495 Invocations: []string{"sourceverdict17"}, 496 }, 497 { 498 Position: 128, 499 Invocations: []string{"sourceverdict9-part1", "sourceverdict9-part2"}, 500 }, 501 { 502 Position: 120, 503 Invocations: []string{"sourceverdict7"}, 504 }, 505 { 506 Position: 120, 507 Invocations: []string{"sourceverdict6"}, 508 }, 509 { 510 Position: 100, 511 Invocations: []string{"sourceverdict3-part1", "sourceverdict3-part2"}, 512 Changelists: []*pb.Changelist{ 513 { 514 Host: "mygerrit-review.googlesource.com", 515 Change: 1, 516 Patchset: 5, 517 OwnerKind: pb.ChangelistOwnerKind_HUMAN, 518 }, 519 }, 520 }, 521 { 522 Position: 100, 523 Invocations: []string{"sourceverdict2"}, 524 }, 525 }, 526 StartPosition: 100, 527 EndPosition: 140, 528 }, 529 }, 530 { 531 TestId: "test_id", 532 Variant: var3, 533 FailureRate: &pb.TestVariantStabilityAnalysis_FailureRate{ 534 IsMet: false, 535 UnexpectedTestRuns: 1, 536 ConsecutiveUnexpectedTestRuns: 1, 537 RecentVerdicts: []*pb.TestVariantStabilityAnalysis_FailureRate_RecentVerdict{ 538 { 539 Position: 130, 540 Invocations: []string{"sourceverdict11"}, 541 UnexpectedRuns: 1, 542 TotalRuns: 2, 543 }, 544 }, 545 }, 546 FlakeRate: &pb.TestVariantStabilityAnalysis_FlakeRate{ 547 IsMet: false, 548 RunFlakyVerdicts: 1, 549 TotalVerdicts: 1, 550 FlakeExamples: []*pb.TestVariantStabilityAnalysis_FlakeRate_VerdictExample{ 551 { 552 Position: 130, 553 Invocations: []string{"sourceverdict11"}, 554 }, 555 }, 556 StartPosition: 130, 557 EndPosition: 130, 558 }, 559 }, 560 } 561 return analysis 562 } 563 564 // QueryStabilitySampleResponseLargeWindow is the expected response from 565 // QueryStability for FlakeRate.MinWindow = 100. 566 func QueryStabilitySampleResponseLargeWindow() []*pb.TestVariantStabilityAnalysis { 567 rsp := QueryStabilitySampleResponse() 568 fr := rsp[0].FlakeRate 569 fr.TotalVerdicts++ 570 fr.RunFlakyVerdicts++ 571 572 // Include verdicts outside the normal window, but within the last 14 days. 573 fr.FlakeExamples = append(fr.FlakeExamples, &pb.TestVariantStabilityAnalysis_FlakeRate_VerdictExample{ 574 Position: 90, 575 Invocations: []string{"sourceverdict1"}, 576 }) 577 fr.StartPosition = 90 578 579 return rsp 580 }