go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/testresults/stability/query_stability_test.go (about) 1 // Copyright 2024 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 "fmt" 19 "testing" 20 "time" 21 22 "go.chromium.org/luci/server/span" 23 24 "go.chromium.org/luci/analysis/internal/testutil" 25 "go.chromium.org/luci/analysis/pbutil" 26 pb "go.chromium.org/luci/analysis/proto/v1" 27 28 . "github.com/smartystreets/goconvey/convey" 29 . "go.chromium.org/luci/common/testing/assertions" 30 ) 31 32 func TestQueryStability(t *testing.T) { 33 Convey("QueryStability", t, func() { 34 ctx := testutil.IntegrationTestContext(t) 35 36 var1 := pbutil.Variant("key1", "val1", "key2", "val1") 37 var3 := pbutil.Variant("key1", "val2", "key2", "val2") 38 39 err := CreateQueryStabilityTestData(ctx) 40 So(err, ShouldBeNil) 41 42 opts := QueryStabilitySampleRequest() 43 expectedResult := QueryStabilitySampleResponse() 44 txn, cancel := span.ReadOnlyTransaction(ctx) 45 defer cancel() 46 47 Convey("Baseline", func() { 48 result, err := QueryStability(txn, opts) 49 So(err, ShouldBeNil) 50 So(result, ShouldResembleProto, expectedResult) 51 }) 52 Convey("Flake analysis uses full 14 days if MinWindow unmet", func() { 53 opts.Criteria.FlakeRate.MinWindow = 100 54 result, err := QueryStability(txn, opts) 55 So(err, ShouldBeNil) 56 So(result, ShouldResembleProto, QueryStabilitySampleResponseLargeWindow()) 57 }) 58 Convey("Project filter works correctly", func() { 59 opts.Project = "none" 60 expectedResult = []*pb.TestVariantStabilityAnalysis{ 61 emptyStabilityAnalysis("test_id", var1), 62 emptyStabilityAnalysis("test_id", var3), 63 } 64 65 result, err := QueryStability(txn, opts) 66 So(err, ShouldBeNil) 67 So(result, ShouldResembleProto, expectedResult) 68 }) 69 Convey("Realm filter works correctly", func() { 70 // No data exists in this realm. 71 opts.SubRealms = []string{"otherrealm"} 72 expectedResult = []*pb.TestVariantStabilityAnalysis{ 73 emptyStabilityAnalysis("test_id", var1), 74 emptyStabilityAnalysis("test_id", var3), 75 } 76 77 result, err := QueryStability(txn, opts) 78 So(err, ShouldBeNil) 79 So(result, ShouldResembleProto, expectedResult) 80 }) 81 Convey("Works for tests without data", func() { 82 notExistsVariant := pbutil.Variant("key1", "val1", "key2", "not_exists") 83 opts.TestVariantPositions = append(opts.TestVariantPositions, 84 &pb.QueryTestVariantStabilityRequest_TestVariantPosition{ 85 TestId: "not_exists_test_id", 86 Variant: var1, 87 Sources: &pb.Sources{ 88 GitilesCommit: &pb.GitilesCommit{ 89 Host: "mysources.googlesource.com", 90 Project: "myproject/src", 91 Ref: "refs/heads/mybranch", 92 CommitHash: "aabbccddeeff00112233aabbccddeeff00112233", 93 Position: 130, 94 }, 95 }, 96 }, 97 &pb.QueryTestVariantStabilityRequest_TestVariantPosition{ 98 TestId: "test_id", 99 Variant: notExistsVariant, 100 Sources: &pb.Sources{ 101 GitilesCommit: &pb.GitilesCommit{ 102 Host: "mysources.googlesource.com", 103 Project: "myproject/src", 104 Ref: "refs/heads/mybranch", 105 CommitHash: "aabbccddeeff00112233aabbccddeeff00112233", 106 Position: 130, 107 }, 108 }, 109 }) 110 111 expectedResult = append(expectedResult, 112 emptyStabilityAnalysis("not_exists_test_id", var1), 113 emptyStabilityAnalysis("test_id", notExistsVariant)) 114 115 result, err := QueryStability(txn, opts) 116 So(err, ShouldBeNil) 117 So(result, ShouldResembleProto, expectedResult) 118 }) 119 Convey("Batching works correctly", func() { 120 // Ensure the order of test variants in the request and response 121 // remain correct even when there are multiple batches. 122 var expandedInput []*pb.QueryTestVariantStabilityRequest_TestVariantPosition 123 var expectedOutput []*pb.TestVariantStabilityAnalysis 124 for i := 0; i < batchSize; i++ { 125 testID := fmt.Sprintf("test_id_%v", i) 126 expandedInput = append(expandedInput, &pb.QueryTestVariantStabilityRequest_TestVariantPosition{ 127 TestId: testID, 128 Variant: var1, 129 Sources: &pb.Sources{ 130 GitilesCommit: &pb.GitilesCommit{ 131 Host: "mysources.googlesource.com", 132 Project: "myproject/src", 133 Ref: "refs/heads/mybranch", 134 CommitHash: "aabbccddeeff00112233aabbccddeeff00112233", 135 Position: 130, 136 }, 137 }, 138 }) 139 expectedOutput = append(expectedOutput, emptyStabilityAnalysis(testID, var1)) 140 } 141 142 opts.TestVariantPositions = append(expandedInput, opts.TestVariantPositions...) 143 expectedResult = append(expectedOutput, expectedResult...) 144 145 result, err := QueryStability(txn, opts) 146 So(err, ShouldBeNil) 147 So(result, ShouldResembleProto, expectedResult) 148 }) 149 }) 150 } 151 152 // emptyStabilityAnalysis returns an empty stability analysis proto. 153 func emptyStabilityAnalysis(testID string, variant *pb.Variant) *pb.TestVariantStabilityAnalysis { 154 return &pb.TestVariantStabilityAnalysis{ 155 TestId: testID, 156 Variant: variant, 157 FailureRate: &pb.TestVariantStabilityAnalysis_FailureRate{}, 158 FlakeRate: &pb.TestVariantStabilityAnalysis_FlakeRate{}, 159 } 160 } 161 162 func TestQueryStabilityHelpers(t *testing.T) { 163 Convey("flattenSourceVerdictsToRuns", t, func() { 164 unexpectedRun := run{expected: false} 165 expectedRun := run{expected: true} 166 Convey("With no verdicts", func() { 167 verdicts := []*sourceVerdict{} 168 result := flattenSourceVerdictsToRuns(verdicts) 169 So(result, ShouldHaveLength, 0) 170 }) 171 Convey("With one unexpected run", func() { 172 verdicts := []*sourceVerdict{ 173 { 174 UnexpectedRuns: 1, 175 }, 176 } 177 result := flattenSourceVerdictsToRuns(verdicts) 178 So(result, ShouldResemble, []run{unexpectedRun}) 179 }) 180 Convey("With many unexpected runs", func() { 181 verdicts := []*sourceVerdict{ 182 { 183 UnexpectedRuns: 3, 184 }, 185 } 186 result := flattenSourceVerdictsToRuns(verdicts) 187 So(result, ShouldResemble, []run{unexpectedRun, unexpectedRun, unexpectedRun}) 188 }) 189 Convey("With one expected run", func() { 190 verdicts := []*sourceVerdict{ 191 { 192 ExpectedRuns: 1, 193 }, 194 } 195 result := flattenSourceVerdictsToRuns(verdicts) 196 So(result, ShouldResemble, []run{expectedRun}) 197 }) 198 Convey("With many expected run", func() { 199 verdicts := []*sourceVerdict{ 200 { 201 ExpectedRuns: 3, 202 }, 203 } 204 result := flattenSourceVerdictsToRuns(verdicts) 205 So(result, ShouldResemble, []run{expectedRun, expectedRun, expectedRun}) 206 }) 207 Convey("With mixed runs, evenly split", func() { 208 verdicts := []*sourceVerdict{ 209 { 210 ExpectedRuns: 3, 211 UnexpectedRuns: 3, 212 }, 213 } 214 result := flattenSourceVerdictsToRuns(verdicts) 215 So(result, ShouldResemble, []run{unexpectedRun, expectedRun, unexpectedRun, expectedRun, unexpectedRun, expectedRun}) 216 }) 217 Convey("With mixed runs, 3/2 split", func() { 218 verdicts := []*sourceVerdict{ 219 { 220 ExpectedRuns: 4, 221 UnexpectedRuns: 2, 222 }, 223 } 224 result := flattenSourceVerdictsToRuns(verdicts) 225 So(result, ShouldResemble, []run{unexpectedRun, expectedRun, expectedRun, unexpectedRun, expectedRun, expectedRun}) 226 }) 227 Convey("With multiple verdicts", func() { 228 verdicts := []*sourceVerdict{ 229 { 230 ExpectedRuns: 2, 231 }, 232 { 233 ExpectedRuns: 1, 234 UnexpectedRuns: 1, 235 }, 236 { 237 UnexpectedRuns: 2, 238 }, 239 } 240 result := flattenSourceVerdictsToRuns(verdicts) 241 So(result, ShouldResemble, []run{expectedRun, expectedRun, unexpectedRun, expectedRun, unexpectedRun, unexpectedRun}) 242 }) 243 }) 244 Convey("truncateSourceVerdicts", t, func() { 245 Convey("With no verdicts", func() { 246 verdicts := []*sourceVerdict{} 247 result := truncateSourceVerdicts(verdicts, 10) 248 So(result, ShouldHaveLength, 0) 249 }) 250 Convey("With large expected verdict", func() { 251 verdicts := []*sourceVerdict{ 252 { 253 ExpectedRuns: 11, 254 }, 255 } 256 result := truncateSourceVerdicts(verdicts, 10) 257 So(result, ShouldResemble, []*sourceVerdict{ 258 { 259 ExpectedRuns: 10, 260 }, 261 }) 262 }) 263 Convey("With large unexpected verdict", func() { 264 verdicts := []*sourceVerdict{ 265 { 266 UnexpectedRuns: 111, 267 }, 268 } 269 result := truncateSourceVerdicts(verdicts, 10) 270 So(result, ShouldResemble, []*sourceVerdict{ 271 { 272 UnexpectedRuns: 10, 273 }, 274 }) 275 }) 276 Convey("With multiple verdicts", func() { 277 verdicts := []*sourceVerdict{ 278 { 279 ExpectedRuns: 2, 280 }, 281 { 282 ExpectedRuns: 8, 283 UnexpectedRuns: 8, 284 }, 285 { 286 UnexpectedRuns: 3, 287 }, 288 } 289 result := truncateSourceVerdicts(verdicts, 10) 290 So(result, ShouldResemble, []*sourceVerdict{ 291 { 292 ExpectedRuns: 2, 293 }, 294 { 295 ExpectedRuns: 4, 296 UnexpectedRuns: 4, 297 }, 298 }) 299 }) 300 }) 301 Convey("consecutiveFailureCount", t, func() { 302 Convey("Consecutive from start and/or end", func() { 303 type testCase struct { 304 runs []run 305 expected int 306 } 307 308 // Assume 10 runs, split 4 after / 2 on / 4 before. 309 testCases := []testCase{ 310 { 311 runs: expectedRuns(10), 312 expected: 0, 313 }, 314 { 315 runs: combine(unexpectedRuns(1), expectedRuns(9)), 316 expected: 0, 317 }, 318 { 319 runs: combine(unexpectedRuns(2), expectedRuns(8)), 320 expected: 0, 321 }, 322 { 323 runs: combine(unexpectedRuns(3), expectedRuns(7)), 324 expected: 0, 325 }, 326 { 327 runs: combine(unexpectedRuns(4), expectedRuns(6)), 328 expected: 0, 329 }, 330 { 331 runs: combine(unexpectedRuns(5), expectedRuns(5)), 332 expected: 0, 333 }, 334 { 335 runs: combine(unexpectedRuns(6), expectedRuns(4)), 336 expected: 6, 337 }, 338 { 339 runs: combine(unexpectedRuns(7), expectedRuns(3)), 340 expected: 7, 341 }, 342 { 343 runs: combine(unexpectedRuns(8), expectedRuns(2)), 344 expected: 8, 345 }, 346 { 347 runs: combine(unexpectedRuns(9), expectedRuns(1)), 348 expected: 9, 349 }, 350 { 351 runs: unexpectedRuns(10), 352 expected: 10, 353 }, 354 { 355 runs: combine(expectedRuns(1), unexpectedRuns(9)), 356 expected: 9, 357 }, 358 { 359 runs: combine(expectedRuns(2), unexpectedRuns(8)), 360 expected: 8, 361 }, 362 { 363 runs: combine(expectedRuns(3), unexpectedRuns(7)), 364 expected: 7, 365 }, 366 { 367 runs: combine(expectedRuns(4), unexpectedRuns(6)), 368 expected: 6, 369 }, 370 { 371 runs: combine(expectedRuns(5), unexpectedRuns(5)), 372 expected: 0, 373 }, 374 } 375 376 for _, tc := range testCases { 377 afterRuns := tc.runs[:4] 378 onRuns := tc.runs[4:6] 379 beforeRuns := tc.runs[6:] 380 So(consecutiveUnexpectedCount(afterRuns, onRuns, beforeRuns), ShouldEqual, tc.expected) 381 } 382 }) 383 Convey("Consecutive runs do not touch start or end", func() { 384 runs := combine(expectedRuns(1), unexpectedRuns(8), expectedRuns(1)) 385 386 afterRuns := runs[:4] 387 onRuns := runs[4:6] 388 beforeRuns := runs[6:] 389 So(consecutiveUnexpectedCount(afterRuns, onRuns, beforeRuns), ShouldEqual, 0) 390 }) 391 Convey("Consecutive unexpected runs on after side of queried position, no runs on queried position", func() { 392 runs := combine(unexpectedRuns(5), expectedRuns(5)) 393 394 afterRuns := runs[:5] 395 onRuns := runs[5:5] // Empty slice 396 beforeRuns := runs[5:] 397 So(consecutiveUnexpectedCount(afterRuns, onRuns, beforeRuns), ShouldEqual, 5) 398 }) 399 Convey("Consecutive unexpected runs on before side of queried position, no runs on queried position", func() { 400 runs := combine(expectedRuns(5), unexpectedRuns(5)) 401 402 afterRuns := runs[:5] 403 onRuns := runs[5:5] // Empty slice 404 beforeRuns := runs[5:] 405 So(consecutiveUnexpectedCount(afterRuns, onRuns, beforeRuns), ShouldEqual, 5) 406 }) 407 }) 408 Convey("unexpectedRunsInWindow", t, func() { 409 Convey("no runs", func() { 410 So(unexpectedRunsInWindow(nil, 10), ShouldEqual, 0) 411 }) 412 Convey("fewer runs than window size", func() { 413 runs := combine(unexpectedRuns(3), expectedRuns(2), unexpectedRuns(2)) 414 So(unexpectedRunsInWindow(runs, 10), ShouldEqual, 5) 415 }) 416 Convey("only expected runs", func() { 417 runs := expectedRuns(20) 418 So(unexpectedRunsInWindow(runs, 10), ShouldEqual, 0) 419 }) 420 Convey("only unexpected runs", func() { 421 runs := unexpectedRuns(20) 422 So(unexpectedRunsInWindow(runs, 10), ShouldEqual, 10) 423 }) 424 Convey("mixed runs", func() { 425 runs := combine(expectedRuns(5), unexpectedRuns(9), expectedRuns(6)) 426 So(unexpectedRunsInWindow(runs, 10), ShouldEqual, 9) 427 }) 428 Convey("mixed runs 2", func() { 429 runs := combine(expectedRuns(1), unexpectedRuns(4), expectedRuns(3), unexpectedRuns(4), expectedRuns(9)) 430 So(unexpectedRunsInWindow(runs, 10), ShouldEqual, 7) 431 }) 432 }) 433 Convey("queryBuckets", t, func() { 434 buckets := []*sourcePositionBucket{ 435 { 436 StartSourcePosition: 2, 437 EndSourcePosition: 3, 438 EarliestPartitionTime: time.Date(2100, time.July, 1, 0, 0, 0, 0, time.UTC), 439 }, 440 { 441 StartSourcePosition: 4, 442 EndSourcePosition: 6, 443 EarliestPartitionTime: time.Date(2100, time.July, 3, 0, 0, 0, 0, time.UTC), 444 }, 445 { 446 StartSourcePosition: 8, 447 EndSourcePosition: 8, 448 EarliestPartitionTime: time.Date(2100, time.July, 6, 0, 0, 0, 0, time.UTC), 449 }, 450 { 451 StartSourcePosition: 10, 452 EndSourcePosition: 13, 453 EarliestPartitionTime: time.Date(2100, time.July, 8, 0, 0, 0, 0, time.UTC), 454 }, 455 { 456 StartSourcePosition: 18, 457 EndSourcePosition: 32, 458 EarliestPartitionTime: time.Date(2100, time.July, 14, 0, 0, 0, 0, time.UTC), 459 // Earliest availability: July 12th (due to bucket below). 460 }, 461 { 462 StartSourcePosition: 34, 463 EndSourcePosition: 40, 464 EarliestPartitionTime: time.Date(2100, time.July, 12, 0, 0, 0, 0, time.UTC), 465 }, 466 } 467 Convey("query at end", func() { 468 result := queryBuckets(buckets, 40, time.Hour*24*7) 469 So(result, ShouldResemble, buckets[2:]) 470 }) 471 Convey("query beyond end", func() { 472 result := queryBuckets(buckets, 50, time.Hour*24*7) 473 So(result, ShouldResemble, buckets[2:]) 474 }) 475 Convey("query in middle", func() { 476 result := queryBuckets(buckets, 8, time.Hour*24*7) 477 So(result, ShouldResemble, buckets) 478 }) 479 Convey("query at start", func() { 480 result := queryBuckets(buckets, 2, time.Hour*24*7) 481 So(result, ShouldResemble, buckets[:4]) 482 }) 483 Convey("query before start", func() { 484 result := queryBuckets(buckets, 1, time.Hour*24*7) 485 So(result, ShouldResemble, buckets[:4]) 486 }) 487 Convey("query empty buckets", func() { 488 result := queryBuckets(nil, 50, time.Hour*24*7) 489 So(result, ShouldHaveLength, 0) 490 }) 491 }) 492 } 493 494 func unexpectedRuns(count int) []run { 495 var result []run 496 for i := 0; i < count; i++ { 497 result = append(result, run{expected: false}) 498 } 499 return result 500 } 501 502 func expectedRuns(count int) []run { 503 var result []run 504 for i := 0; i < count; i++ { 505 result = append(result, run{expected: true}) 506 } 507 return result 508 } 509 510 func combine(runs ...[]run) []run { 511 var result []run 512 for _, runs := range runs { 513 result = append(result, runs...) 514 } 515 return result 516 }