github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/summarizer/flakiness_test.go (about) 1 /* 2 Copyright 2020 The TestGrid Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package summarizer 18 19 import ( 20 "testing" 21 22 statepb "github.com/GoogleCloudPlatform/testgrid/pb/state" 23 summarypb "github.com/GoogleCloudPlatform/testgrid/pb/summary" 24 statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status" 25 "github.com/GoogleCloudPlatform/testgrid/pkg/summarizer/analyzers" 26 "github.com/GoogleCloudPlatform/testgrid/pkg/summarizer/common" 27 "github.com/golang/protobuf/proto" 28 "github.com/google/go-cmp/cmp" 29 "github.com/google/go-cmp/cmp/cmpopts" 30 ) 31 32 func TestCalculateTrend(t *testing.T) { 33 cases := []struct { 34 name string 35 currentHealthiness *summarypb.HealthinessInfo 36 previousHealthiness *summarypb.HealthinessInfo 37 expected *summarypb.HealthinessInfo 38 }{ 39 { 40 name: "typical input assigns correct ChangeFromLastInterval's", 41 currentHealthiness: &summarypb.HealthinessInfo{ 42 Tests: []*summarypb.TestInfo{ 43 { 44 DisplayName: "test2_should_be_DOWN", 45 Flakiness: 30.0, 46 }, 47 { 48 DisplayName: "test1_should_be_UP", 49 Flakiness: 70.0, 50 }, 51 { 52 DisplayName: "test3_should_be_NO_CHANGE", 53 Flakiness: 50.0, 54 }, 55 }, 56 }, 57 previousHealthiness: &summarypb.HealthinessInfo{ 58 Tests: []*summarypb.TestInfo{ 59 { 60 DisplayName: "test1_should_be_UP", 61 Flakiness: 50.0, 62 }, 63 { 64 DisplayName: "test2_should_be_DOWN", 65 Flakiness: 50.0, 66 }, 67 { 68 DisplayName: "test3_should_be_NO_CHANGE", 69 Flakiness: 50.0, 70 }, 71 }, 72 }, 73 expected: &summarypb.HealthinessInfo{ 74 Tests: []*summarypb.TestInfo{ 75 { 76 DisplayName: "test2_should_be_DOWN", 77 Flakiness: 30.0, 78 PreviousFlakiness: []float32{50.0}, 79 ChangeFromLastInterval: summarypb.TestInfo_DOWN, 80 }, 81 { 82 DisplayName: "test1_should_be_UP", 83 Flakiness: 70.0, 84 PreviousFlakiness: []float32{50.0}, 85 ChangeFromLastInterval: summarypb.TestInfo_UP, 86 }, 87 { 88 DisplayName: "test3_should_be_NO_CHANGE", 89 Flakiness: 50.0, 90 PreviousFlakiness: []float32{50.0}, 91 ChangeFromLastInterval: summarypb.TestInfo_NO_CHANGE, 92 }, 93 }, 94 }, 95 }, 96 } 97 98 for _, tc := range cases { 99 t.Run(tc.name, func(t *testing.T) { 100 if CalculateTrend(tc.currentHealthiness, tc.previousHealthiness); !proto.Equal(tc.currentHealthiness, tc.expected) { 101 for _, expectedTest := range tc.expected.Tests { 102 // Linear search because the test cases are so small 103 for _, actualTest := range tc.currentHealthiness.Tests { 104 if actualTest.DisplayName != expectedTest.DisplayName { 105 continue 106 } 107 actual := actualTest.ChangeFromLastInterval 108 expected := expectedTest.ChangeFromLastInterval 109 if actual == expected { 110 continue 111 } 112 113 actualValue := int(actualTest.ChangeFromLastInterval) 114 expectedValue := int(expectedTest.ChangeFromLastInterval) 115 t.Logf("test: %s has trend of: %s (value: %d) but expected %s (value: %d)", 116 actualTest.DisplayName, actual, actualValue, expected, expectedValue) 117 } 118 } 119 t.Fail() 120 } 121 }) 122 } 123 } 124 125 func TestGetTrend(t *testing.T) { 126 cases := []struct { 127 name string 128 currentFlakiness float32 129 previousFlakiness float32 130 expected summarypb.TestInfo_Trend 131 }{ 132 { 133 name: "lower currentFlakiness returns TestInfo_DOWN", 134 currentFlakiness: 10.0, 135 previousFlakiness: 20.0, 136 expected: summarypb.TestInfo_DOWN, 137 }, 138 { 139 name: "higher currentFlakiness returns TestInfo_UP", 140 currentFlakiness: 20.0, 141 previousFlakiness: 10.0, 142 expected: summarypb.TestInfo_UP, 143 }, 144 { 145 name: "equal currentFlakiness and previousFlakiness returns TestInfo_NO_CHANGE", 146 currentFlakiness: 5.0, 147 previousFlakiness: 5.0, 148 expected: summarypb.TestInfo_NO_CHANGE, 149 }, 150 } 151 for _, tc := range cases { 152 t.Run(tc.name, func(t *testing.T) { 153 if actual := getTrend(tc.currentFlakiness, tc.previousFlakiness); actual != tc.expected { 154 t.Errorf("getTrend returned actual: %d != expected: %d for inputs (%f, %f)", actual, tc.expected, tc.currentFlakiness, tc.previousFlakiness) 155 } 156 }) 157 } 158 } 159 160 func TestIsWithinTimeFrame(t *testing.T) { 161 cases := []struct { 162 name string 163 column *statepb.Column 164 startTime int 165 endTime int 166 expected bool 167 }{ 168 { 169 name: "column within time frame returns true", 170 column: &statepb.Column{ 171 Started: 1.0, 172 }, 173 startTime: 0, 174 endTime: 2, 175 expected: true, 176 }, 177 { 178 name: "column before time frame returns false", 179 column: &statepb.Column{ 180 Started: 1.0, 181 }, 182 startTime: 3, 183 endTime: 7, 184 expected: false, 185 }, 186 { 187 name: "column after time frame returns false", 188 column: &statepb.Column{ 189 Started: 4.0, 190 }, 191 startTime: 0, 192 endTime: 2, 193 expected: false, 194 }, 195 { 196 name: "function is inclusive with column at start time", 197 column: &statepb.Column{ 198 Started: 0.0, 199 }, 200 startTime: 0, 201 endTime: 2, 202 expected: true, 203 }, 204 { 205 name: "function is inclusive with column at end time", 206 column: &statepb.Column{ 207 Started: 2.0, 208 }, 209 startTime: 0, 210 endTime: 2, 211 expected: true, 212 }, 213 } 214 215 for _, tc := range cases { 216 t.Run(tc.name, func(t *testing.T) { 217 if actual := isWithinTimeFrame(tc.column, tc.startTime, tc.endTime); actual != tc.expected { 218 t.Errorf("isWithinTimeFrame returned %t for %d < %f <= %d", actual, tc.startTime, tc.column.Started, tc.endTime) 219 } 220 }) 221 } 222 } 223 224 func TestParseGrid(t *testing.T) { 225 cases := []struct { 226 name string 227 grid *statepb.Grid 228 startTime int 229 endTime int 230 expectedMetrics []*common.GridMetrics 231 expectedFilteredStatus map[string][]analyzers.StatusCategory 232 }{ 233 { 234 name: "grid with all analyzed result types produces correct result list", 235 grid: &statepb.Grid{ 236 Columns: []*statepb.Column{ 237 {Started: 0}, 238 {Started: 1000}, 239 {Started: 2000}, 240 {Started: 2000}, 241 }, 242 Rows: []*statepb.Row{ 243 { 244 Name: "test_1", 245 Results: []int32{ 246 statuspb.TestStatus_value["PASS"], 1, 247 statuspb.TestStatus_value["FAIL"], 1, 248 statuspb.TestStatus_value["FLAKY"], 1, 249 statuspb.TestStatus_value["CATEGORIZED_FAIL"], 1, 250 }, 251 Messages: []string{ 252 "", 253 "", 254 "", 255 "infra_fail_1", 256 }, 257 }, 258 }, 259 }, 260 startTime: 0, 261 endTime: 2, 262 expectedMetrics: []*common.GridMetrics{ 263 { 264 Name: "test_1", 265 Passed: 1, 266 Failed: 1, 267 FlakyCount: 1, 268 AverageFlakiness: 50.0, 269 FailedInfraCount: 1, 270 InfraFailures: map[string]int{ 271 "infra_fail_1": 1, 272 }, 273 }, 274 }, 275 expectedFilteredStatus: map[string][]analyzers.StatusCategory{ 276 "test_1": { 277 analyzers.StatusPass, analyzers.StatusFail, analyzers.StatusFlaky, 278 }, 279 }, 280 }, 281 { 282 name: "grid with failing columns produces correct status list", 283 grid: &statepb.Grid{ 284 Columns: []*statepb.Column{ 285 {Started: 0}, 286 {Started: 1000}, 287 {Started: 2000}, 288 {Started: 2000}, 289 }, 290 Rows: []*statepb.Row{ 291 { 292 Name: "test_1", 293 Results: []int32{ 294 statuspb.TestStatus_value["PASS"], 1, 295 statuspb.TestStatus_value["FAIL"], 1, 296 statuspb.TestStatus_value["FLAKY"], 1, 297 statuspb.TestStatus_value["CATEGORIZED_FAIL"], 1, 298 }, 299 Messages: []string{ 300 "", 301 "", 302 "", 303 "infra_fail_1", 304 }, 305 }, 306 { 307 Name: "test_2", 308 Results: []int32{ 309 statuspb.TestStatus_value["PASS"], 1, 310 statuspb.TestStatus_value["FAIL"], 1, 311 statuspb.TestStatus_value["FAIL"], 1, 312 statuspb.TestStatus_value["CATEGORIZED_FAIL"], 1, 313 }, 314 Messages: []string{ 315 "", 316 "", 317 "", 318 "infra_fail_1", 319 }, 320 }, 321 }, 322 }, 323 startTime: 0, 324 endTime: 2, 325 expectedMetrics: []*common.GridMetrics{ 326 { 327 Name: "test_1", 328 Passed: 1, 329 Failed: 1, 330 FlakyCount: 1, 331 AverageFlakiness: 50.0, 332 FailedInfraCount: 1, 333 InfraFailures: map[string]int{ 334 "infra_fail_1": 1, 335 }, 336 }, 337 { 338 Name: "test_2", 339 Passed: 1, 340 Failed: 2, 341 FlakyCount: 0, 342 AverageFlakiness: 2 / 3, 343 FailedInfraCount: 1, 344 InfraFailures: map[string]int{ 345 "infra_fail_1": 1, 346 }, 347 }, 348 }, 349 expectedFilteredStatus: map[string][]analyzers.StatusCategory{ 350 "test_1": { 351 analyzers.StatusPass, analyzers.StatusFlaky, 352 }, 353 "test_2": { 354 analyzers.StatusPass, analyzers.StatusFail, 355 }, 356 }, 357 }, 358 { 359 name: "grid with no analyzed results produces empty result list", 360 grid: &statepb.Grid{ 361 Columns: []*statepb.Column{ 362 {Started: -1000}, 363 {Started: 1000}, 364 {Started: 2000}, 365 {Started: 2000}, 366 }, 367 Rows: []*statepb.Row{ 368 { 369 Name: "test_1", 370 Results: []int32{ 371 statuspb.TestStatus_value["NO_RESULT"], 4, 372 }, 373 Messages: []string{ 374 "this_message_should_not_show_up_in_results_0", 375 "this_message_should_not_show_up_in_results_1", 376 "this_message_should_not_show_up_in_results_2", 377 "this_message_should_not_show_up_in_results_3", 378 }, 379 }, 380 }, 381 }, 382 startTime: 0, 383 endTime: 2, 384 expectedMetrics: []*common.GridMetrics{}, 385 expectedFilteredStatus: map[string][]analyzers.StatusCategory{ 386 "test_1": {}, 387 }, 388 }, 389 { 390 name: "grid with some non-analyzed results properly assigns correct messages", 391 grid: &statepb.Grid{ 392 Columns: []*statepb.Column{ 393 {Started: 0}, 394 {Started: 1000}, 395 {Started: 1000}, 396 {Started: 2000}, 397 {Started: 2000}, 398 }, 399 Rows: []*statepb.Row{ 400 { 401 Name: "test_1", 402 Results: []int32{ 403 statuspb.TestStatus_value["PASS"], 1, 404 statuspb.TestStatus_value["NO_RESULT"], 2, 405 statuspb.TestStatus_value["FAIL"], 2, 406 }, 407 Messages: []string{ 408 "this_message_should_not_show_up_in_results", 409 "this_message_should_show_up_as_an_infra_failure", 410 "", 411 }, 412 }, 413 }, 414 }, 415 startTime: 0, 416 endTime: 2, 417 expectedMetrics: []*common.GridMetrics{ 418 { 419 Name: "test_1", 420 Passed: 1, 421 Failed: 1, 422 FlakyCount: 0, 423 AverageFlakiness: 0.0, 424 FailedInfraCount: 1, 425 InfraFailures: map[string]int{ 426 "this_message_should_show_up_as_an_infra_failure": 1, 427 }, 428 }, 429 }, 430 expectedFilteredStatus: map[string][]analyzers.StatusCategory{ 431 "test_1": { 432 analyzers.StatusPass, analyzers.StatusFail, 433 }, 434 }, 435 }, 436 { 437 name: "grid with columns outside of time frame correctly assigns messages", 438 grid: &statepb.Grid{ 439 Columns: []*statepb.Column{ 440 {Started: 0}, 441 {Started: 1000}, 442 {Started: 1000}, 443 {Started: 7000}, 444 {Started: 2000}, 445 }, 446 Rows: []*statepb.Row{ 447 { 448 Name: "test_1", 449 Results: []int32{ 450 statuspb.TestStatus_value["PASS"], 1, 451 statuspb.TestStatus_value["NO_RESULT"], 2, 452 statuspb.TestStatus_value["FAIL"], 2, 453 }, 454 Messages: []string{ 455 "this_message_should_not_show_up_in_results", 456 "this_message_should_not_show_up_in_results", 457 "this_message_should_show_up_as_an_infra_failure", 458 }, 459 }, 460 }, 461 }, 462 startTime: 0, 463 endTime: 2, 464 expectedMetrics: []*common.GridMetrics{ 465 { 466 Name: "test_1", 467 Passed: 1, 468 Failed: 0, 469 FlakyCount: 0, 470 AverageFlakiness: 0.0, 471 FailedInfraCount: 1, 472 InfraFailures: map[string]int{ 473 "this_message_should_show_up_as_an_infra_failure": 1, 474 }, 475 }, 476 }, 477 expectedFilteredStatus: map[string][]analyzers.StatusCategory{ 478 "test_1": { 479 analyzers.StatusPass, 480 }, 481 }, 482 }, 483 } 484 485 metricsSort := func(x *common.GridMetrics, y *common.GridMetrics) bool { 486 return x.Name < y.Name 487 } 488 489 for _, tc := range cases { 490 t.Run(tc.name, func(t *testing.T) { 491 actualMetrics, actualFS := parseGrid(tc.grid, tc.startTime, tc.endTime) 492 if diff := cmp.Diff(tc.expectedMetrics, actualMetrics, cmpopts.SortSlices(metricsSort)); diff != "" { 493 t.Errorf("Metrics disagree (-want +got):\n%s", diff) 494 } 495 if diff := cmp.Diff(tc.expectedFilteredStatus, actualFS); diff != "" { 496 t.Errorf("Status disagree (-want +got):\n%s", diff) 497 } 498 }) 499 } 500 } 501 502 func TestIsInfraFailure(t *testing.T) { 503 cases := []struct { 504 name string 505 message string 506 expected bool 507 }{ 508 { 509 name: "typical matched string increments counts correctly", 510 message: "whatever_valid_word_character_string_with_no_spaces", 511 expected: true, 512 }, 513 { 514 name: "unmatched string increments Failed and not other counts", 515 message: "message with spaces should no get matched", 516 expected: false, 517 }, 518 } 519 520 for _, tc := range cases { 521 t.Run(tc.name, func(t *testing.T) { 522 if actual := isInfraFailure(tc.message); actual != tc.expected { 523 t.Errorf("isInfraFailure(%v) gave %t but want %t", tc.message, actual, tc.expected) 524 } 525 }) 526 } 527 } 528 529 func TestIsValidTestName(t *testing.T) { 530 cases := []struct { 531 name string 532 testName string 533 expected bool 534 }{ 535 { 536 name: "regular name returns true", 537 testName: "valid_test", 538 expected: true, 539 }, 540 { 541 name: "name with substring '@TESTGRID@' returns false", 542 testName: "invalid_test_@TESTGRID@", 543 expected: false, 544 }, 545 } 546 for _, tc := range cases { 547 t.Run(tc.name, func(t *testing.T) { 548 if actual := isValidTestName(tc.testName); actual != tc.expected { 549 t.Errorf("isValidTestName returned %t for the name %s, but expected %t", actual, tc.testName, tc.expected) 550 } 551 }) 552 } 553 } 554 555 func TestFailingColumns(t *testing.T) { 556 p := statuspb.TestStatus_value["PASS"] 557 f := statuspb.TestStatus_value["FAIL"] 558 fl := statuspb.TestStatus_value["FLAKY"] 559 cases := []struct { 560 name string 561 rows []*statepb.Row 562 numColumns int 563 expected []bool 564 }{ 565 { 566 name: "Some failing columns", 567 rows: []*statepb.Row{ 568 { 569 Name: "//test1 - [env1]", 570 Results: []int32{ 571 p, 1, f, 1, p, 1, p, 1, f, 1, 572 }, 573 }, 574 { 575 Name: "//test2 - [env1]", 576 Results: []int32{ 577 p, 1, f, 1, p, 1, p, 1, f, 1, 578 }, 579 }, 580 { 581 Name: "//test3 - [env1]", 582 Results: []int32{ 583 p, 1, f, 1, p, 1, p, 1, fl, 1, 584 }, 585 }, 586 { 587 Name: "//test4 - [env1]", 588 Results: []int32{ 589 p, 1, f, 1, p, 1, p, 1, f, 1, 590 }, 591 }, 592 }, 593 numColumns: 5, 594 expected: []bool{false, true, false, false, false}, 595 }, 596 { 597 name: "Unequal Length rows", 598 rows: []*statepb.Row{ 599 { 600 Name: "//test1 - [env1]", 601 Results: []int32{ 602 p, 1, f, 1, p, 1, 603 }, 604 }, 605 { 606 Name: "//test2 - [env1]", 607 Results: []int32{ 608 p, 1, f, 1, 609 }, 610 }, 611 { 612 Name: "//test3 - [env1]", 613 Results: []int32{ 614 p, 1, f, 1, p, 1, p, 1, 615 }, 616 }, 617 }, 618 numColumns: 3, 619 expected: []bool{false, true, false}, 620 }, 621 { 622 name: "Only one test", 623 rows: []*statepb.Row{ 624 { 625 Name: "//test1 - [env1]", 626 Results: []int32{ 627 p, 1, f, 1, p, 1, 628 }, 629 }, 630 }, 631 numColumns: 3, 632 expected: []bool{false, false, false}, 633 }, 634 } 635 for _, tc := range cases { 636 t.Run(tc.name, func(t *testing.T) { 637 actual := failingColumns(tc.numColumns, tc.rows) 638 if diff := cmp.Diff(tc.expected, actual); diff != "" { 639 t.Errorf("failingColumns(ctx, %v %v) gave unexpected diff (-want +got): %s", tc.numColumns, tc.rows, diff) 640 } 641 }) 642 } 643 }