github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/summarizer/summary_test.go (about) 1 /* 2 Copyright 2019 The Kubernetes 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 "bytes" 21 "compress/zlib" 22 "context" 23 "errors" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "sort" 28 "testing" 29 "time" 30 31 "bitbucket.org/creachadair/stringset" 32 "cloud.google.com/go/storage" 33 "github.com/golang/protobuf/proto" 34 "github.com/golang/protobuf/ptypes/timestamp" 35 "github.com/google/go-cmp/cmp" 36 "github.com/google/go-cmp/cmp/cmpopts" 37 "github.com/sirupsen/logrus" 38 "google.golang.org/protobuf/testing/protocmp" 39 40 "github.com/GoogleCloudPlatform/testgrid/internal/result" 41 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 42 statepb "github.com/GoogleCloudPlatform/testgrid/pb/state" 43 summarypb "github.com/GoogleCloudPlatform/testgrid/pb/summary" 44 statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status" 45 "github.com/GoogleCloudPlatform/testgrid/util/gcs" 46 "github.com/GoogleCloudPlatform/testgrid/util/gcs/fake" 47 ) 48 49 type fakeGroup struct { 50 group *configpb.TestGroup 51 grid *statepb.Grid 52 mod time.Time 53 gen int64 54 err error 55 } 56 57 func TestUpdate(t *testing.T) { 58 cases := []struct { 59 name string 60 }{ 61 {}, 62 } 63 64 for _, tc := range cases { 65 t.Run(tc.name, func(t *testing.T) { 66 // TODO(fejta): implement 67 }) 68 } 69 } 70 71 func TestUpdateDashboard(t *testing.T) { 72 cases := []struct { 73 name string 74 dash *configpb.Dashboard 75 groups map[string]fakeGroup 76 tabMode bool 77 expected *summarypb.DashboardSummary 78 err bool 79 }{ 80 { 81 name: "basically works", 82 dash: &configpb.Dashboard{ 83 Name: "stale-dashboard", 84 DashboardTab: []*configpb.DashboardTab{ 85 { 86 Name: "stale-tab", 87 TestGroupName: "foo-group", 88 AlertOptions: &configpb.DashboardTabAlertOptions{ 89 AlertStaleResultsHours: 1, 90 }, 91 }, 92 }, 93 }, 94 groups: map[string]fakeGroup{ 95 "foo-group": { 96 group: &configpb.TestGroup{}, 97 grid: &statepb.Grid{}, 98 mod: time.Unix(1000, 0), 99 }, 100 }, 101 expected: &summarypb.DashboardSummary{ 102 TabSummaries: []*summarypb.DashboardTabSummary{ 103 { 104 DashboardName: "stale-dashboard", 105 DashboardTabName: "stale-tab", 106 LastUpdateTimestamp: 1000, 107 Alert: noRuns, 108 OverallStatus: summarypb.DashboardTabSummary_STALE, 109 Status: noRuns, 110 LatestGreen: noGreens, 111 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{}, 112 }, 113 }, 114 }, 115 }, 116 { 117 name: "still update working tabs when some tabs fail", 118 dash: &configpb.Dashboard{ 119 Name: "a-dashboard", 120 DashboardTab: []*configpb.DashboardTab{ 121 { 122 Name: "working", 123 TestGroupName: "working-group", 124 AlertOptions: &configpb.DashboardTabAlertOptions{ 125 AlertStaleResultsHours: 1, 126 }, 127 }, 128 { 129 Name: "missing-tab", 130 TestGroupName: "group-not-present", 131 AlertOptions: &configpb.DashboardTabAlertOptions{ 132 AlertStaleResultsHours: 1, 133 }, 134 }, 135 { 136 Name: "error-tab", 137 TestGroupName: "has-errors", 138 AlertOptions: &configpb.DashboardTabAlertOptions{ 139 AlertStaleResultsHours: 1, 140 }, 141 }, 142 { 143 Name: "still-working", 144 TestGroupName: "working-group", 145 AlertOptions: &configpb.DashboardTabAlertOptions{ 146 AlertStaleResultsHours: 1, 147 }, 148 }, 149 }, 150 }, 151 groups: map[string]fakeGroup{ 152 "working-group": { 153 mod: time.Unix(1000, 0), 154 group: &configpb.TestGroup{}, 155 grid: &statepb.Grid{}, 156 }, 157 "has-errors": { 158 err: errors.New("tragedy"), 159 group: &configpb.TestGroup{}, 160 grid: &statepb.Grid{}, 161 }, 162 }, 163 expected: &summarypb.DashboardSummary{ 164 TabSummaries: []*summarypb.DashboardTabSummary{ 165 { 166 DashboardName: "a-dashboard", 167 DashboardTabName: "working", 168 LastUpdateTimestamp: 1000, 169 Alert: noRuns, 170 Status: noRuns, 171 OverallStatus: summarypb.DashboardTabSummary_STALE, 172 LatestGreen: noGreens, 173 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{}, 174 }, 175 tabStatus("a-dashboard", "missing-tab", `Test group does not exist: "group-not-present"`), 176 tabStatus("a-dashboard", "error-tab", fmt.Sprintf("Error attempting to summarize tab: load has-errors: open: tragedy")), 177 { 178 DashboardName: "a-dashboard", 179 DashboardTabName: "still-working", 180 LastUpdateTimestamp: 1000, 181 Alert: noRuns, 182 Status: noRuns, 183 OverallStatus: summarypb.DashboardTabSummary_STALE, 184 LatestGreen: noGreens, 185 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{}, 186 }, 187 }, 188 }, 189 err: true, 190 }, 191 { 192 name: "bug url", 193 dash: &configpb.Dashboard{ 194 Name: "a-dashboard", 195 DashboardTab: []*configpb.DashboardTab{ 196 { 197 Name: "none", 198 TestGroupName: "a-group", 199 }, 200 { 201 Name: "empty", 202 TestGroupName: "a-group", 203 OpenBugTemplate: &configpb.LinkTemplate{}, 204 }, 205 { 206 Name: "url", 207 TestGroupName: "a-group", 208 OpenBugTemplate: &configpb.LinkTemplate{ 209 Url: "http://some-bugs/", 210 }, 211 }, 212 { 213 Name: "url-options-empty", 214 TestGroupName: "a-group", 215 OpenBugTemplate: &configpb.LinkTemplate{ 216 Url: "http://more-bugs/", 217 Options: []*configpb.LinkOptionsTemplate{}, 218 }, 219 }, 220 { 221 Name: "url-options", 222 TestGroupName: "a-group", 223 OpenBugTemplate: &configpb.LinkTemplate{ 224 Url: "http://ooh-bugs/", 225 Options: []*configpb.LinkOptionsTemplate{ 226 { 227 Key: "id", 228 Value: "warble", 229 }, 230 { 231 Key: "name", 232 Value: "garble", 233 }, 234 }, 235 }, 236 }, 237 }, 238 }, 239 groups: map[string]fakeGroup{ 240 "a-group": { 241 mod: time.Unix(1000, 0), 242 group: &configpb.TestGroup{}, 243 grid: &statepb.Grid{}, 244 }, 245 }, 246 expected: &summarypb.DashboardSummary{ 247 TabSummaries: []*summarypb.DashboardTabSummary{ 248 { 249 DashboardName: "a-dashboard", 250 DashboardTabName: "none", 251 LastUpdateTimestamp: 1000, 252 Status: noRuns, 253 OverallStatus: summarypb.DashboardTabSummary_UNKNOWN, 254 LatestGreen: noGreens, 255 BugUrl: "", 256 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{}, 257 }, 258 { 259 DashboardName: "a-dashboard", 260 DashboardTabName: "empty", 261 LastUpdateTimestamp: 1000, 262 Status: noRuns, 263 OverallStatus: summarypb.DashboardTabSummary_UNKNOWN, 264 LatestGreen: noGreens, 265 BugUrl: "", 266 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{}, 267 }, 268 { 269 DashboardName: "a-dashboard", 270 DashboardTabName: "url", 271 LastUpdateTimestamp: 1000, 272 Status: noRuns, 273 OverallStatus: summarypb.DashboardTabSummary_UNKNOWN, 274 LatestGreen: noGreens, 275 BugUrl: "http://some-bugs/", 276 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{}, 277 }, 278 { 279 DashboardName: "a-dashboard", 280 DashboardTabName: "url-options-empty", 281 LastUpdateTimestamp: 1000, 282 Status: noRuns, 283 OverallStatus: summarypb.DashboardTabSummary_UNKNOWN, 284 LatestGreen: noGreens, 285 BugUrl: "http://more-bugs/", 286 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{}, 287 }, 288 { 289 DashboardName: "a-dashboard", 290 DashboardTabName: "url-options", 291 LastUpdateTimestamp: 1000, 292 Status: noRuns, 293 OverallStatus: summarypb.DashboardTabSummary_UNKNOWN, 294 LatestGreen: noGreens, 295 BugUrl: "http://ooh-bugs/", 296 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{}, 297 }, 298 }, 299 }, 300 }, 301 } 302 303 for _, tc := range cases { 304 tabUpdater := tabUpdatePool(context.Background(), logrus.WithField("name", "pool"), 5, FeatureFlags{false, false, false}) 305 t.Run(tc.name, func(t *testing.T) { 306 finder := func(dash string, tab *configpb.DashboardTab) (*gcs.Path, *configpb.TestGroup, gridReader, error) { 307 name := tab.TestGroupName 308 if name == "inject-error" { 309 return nil, nil, nil, errors.New("injected find group error") 310 } 311 fake, ok := tc.groups[name] 312 if !ok { 313 return nil, nil, nil, nil 314 } 315 var path *gcs.Path 316 var err error 317 318 path, err = gcs.NewPath(fmt.Sprintf("gs://bucket/grid/%s/%s", dash, name)) 319 if err != nil { 320 t.Helper() 321 t.Fatalf("Failed to create path: %v", err) 322 } 323 reader := func(_ context.Context) (io.ReadCloser, time.Time, int64, error) { 324 return ioutil.NopCloser(bytes.NewBuffer(compress(gridBuf(fake.grid)))), fake.mod, fake.gen, fake.err 325 } 326 return path, fake.group, reader, nil 327 } 328 var actual summarypb.DashboardSummary 329 client := fake.Stater{} 330 for name, group := range tc.groups { 331 path, err := gcs.NewPath(fmt.Sprintf("gs://bucket/grid/%s/%s", tc.dash.Name, name)) 332 if err != nil { 333 t.Errorf("Failed to create Path: %v", err) 334 } 335 client[*path] = fake.Stat{ 336 Attrs: storage.ObjectAttrs{ 337 Generation: group.gen, 338 Updated: group.mod, 339 }, 340 } 341 } 342 updateDashboard(context.Background(), client, tc.dash, &actual, finder, tabUpdater) 343 if diff := cmp.Diff(tc.expected, &actual, protocmp.Transform()); diff != "" { 344 t.Errorf("updateDashboard() got unexpected diff (-want +got):\n%s", diff) 345 } 346 }) 347 } 348 } 349 350 func TestFilterDashboards(t *testing.T) { 351 cases := []struct { 352 name string 353 dashboards map[string]*configpb.Dashboard 354 allowed []string 355 want map[string]*configpb.Dashboard 356 }{ 357 { 358 name: "empty", 359 }, 360 { 361 name: "basic", 362 dashboards: map[string]*configpb.Dashboard{ 363 "hello": {Name: "hi"}, 364 }, 365 want: map[string]*configpb.Dashboard{ 366 "hello": {Name: "hi"}, 367 }, 368 }, 369 { 370 name: "zero", 371 dashboards: map[string]*configpb.Dashboard{ 372 "hello": {Name: "hi"}, 373 }, 374 allowed: []string{"nothing"}, 375 want: map[string]*configpb.Dashboard{}, 376 }, 377 { 378 name: "both", 379 dashboards: map[string]*configpb.Dashboard{ 380 "hello": {Name: "hi"}, 381 "world": {Name: "there"}, 382 }, 383 allowed: []string{"hi", "there"}, 384 want: map[string]*configpb.Dashboard{ 385 "hello": {Name: "hi"}, 386 "world": {Name: "there"}, 387 }, 388 }, 389 { 390 name: "one", 391 dashboards: map[string]*configpb.Dashboard{ 392 "hello": {Name: "hi"}, 393 "drop": {Name: "cuss-word"}, 394 "world": {Name: "there"}, 395 }, 396 allowed: []string{"hi", "there", "drop"}, // target name, not key 397 want: map[string]*configpb.Dashboard{ 398 "hello": {Name: "hi"}, 399 "world": {Name: "there"}, 400 }, 401 }, 402 } 403 404 for _, tc := range cases { 405 t.Run(tc.name, func(t *testing.T) { 406 var allowed stringset.Set 407 if tc.allowed != nil { 408 allowed = stringset.New(tc.allowed...) 409 } 410 411 got := filterDashboards(tc.dashboards, allowed) 412 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 413 t.Errorf("filterDashboards() got unexpected diff (-want +got):\n%s", diff) 414 } 415 }) 416 } 417 } 418 419 func TestStaleHours(t *testing.T) { 420 cases := []struct { 421 name string 422 tab *configpb.DashboardTab 423 expected time.Duration 424 }{ 425 { 426 name: "zero without an alert", 427 expected: 0, 428 }, 429 { 430 name: "use defined hours when set", 431 tab: &configpb.DashboardTab{ 432 AlertOptions: &configpb.DashboardTabAlertOptions{ 433 AlertStaleResultsHours: 4, 434 }, 435 }, 436 expected: 4 * time.Hour, 437 }, 438 } 439 440 for _, tc := range cases { 441 t.Run(tc.name, func(t *testing.T) { 442 if tc.tab == nil { 443 tc.tab = &configpb.DashboardTab{} 444 } 445 if actual := staleHours(tc.tab); actual != tc.expected { 446 t.Errorf("actual %v != expected %v", actual, tc.expected) 447 } 448 }) 449 } 450 } 451 452 func gridBuf(grid *statepb.Grid) []byte { 453 buf, err := proto.Marshal(grid) 454 if err != nil { 455 panic(err) 456 } 457 return buf 458 } 459 460 func compress(buf []byte) []byte { 461 var zbuf bytes.Buffer 462 zw := zlib.NewWriter(&zbuf) 463 if _, err := zw.Write(buf); err != nil { 464 panic(err) 465 } 466 if err := zw.Close(); err != nil { 467 panic(err) 468 } 469 return zbuf.Bytes() 470 } 471 472 func TestUpdateTab(t *testing.T) { 473 now := time.Now() 474 cases := []struct { 475 name string 476 tab *configpb.DashboardTab 477 group *configpb.TestGroup 478 grid *statepb.Grid 479 mod time.Time 480 gen int64 481 gridError error 482 features FeatureFlags 483 expected *summarypb.DashboardTabSummary 484 err bool 485 }{ 486 { 487 name: "read grid error returns error", 488 tab: &configpb.DashboardTab{ 489 TestGroupName: "foo", 490 }, 491 group: &configpb.TestGroup{}, 492 mod: now, 493 gen: 42, 494 gridError: errors.New("burninated"), 495 err: true, 496 }, 497 { 498 name: "basically works", // TODO(fejta): more better 499 tab: &configpb.DashboardTab{ 500 Name: "foo-tab", 501 TestGroupName: "foo-group", 502 AlertOptions: &configpb.DashboardTabAlertOptions{ 503 AlertStaleResultsHours: 1, 504 }, 505 }, 506 group: &configpb.TestGroup{}, 507 grid: &statepb.Grid{}, 508 mod: now, 509 gen: 43, 510 expected: &summarypb.DashboardTabSummary{ 511 DashboardTabName: "foo-tab", 512 LastUpdateTimestamp: float64(now.Unix()), 513 Alert: noRuns, 514 LatestGreen: noGreens, 515 OverallStatus: summarypb.DashboardTabSummary_STALE, 516 Status: noRuns, 517 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{}, 518 }, 519 }, 520 { 521 name: "fuzzy flakiness configured and allowed", 522 tab: &configpb.DashboardTab{ 523 Name: "foo-tab", 524 TestGroupName: "foo-group", 525 NumColumnsRecent: 4, 526 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 527 MaxAcceptableFlakiness: 50.0, 528 }, 529 }, 530 group: &configpb.TestGroup{}, 531 grid: &statepb.Grid{ 532 Rows: []*statepb.Row{ 533 { 534 Name: "test-1", 535 Results: []int32{int32(statuspb.TestStatus_PASS), 3, int32(statuspb.TestStatus_FAIL), 1}, 536 }, 537 }, 538 Columns: []*statepb.Column{ 539 { 540 Name: "Uno", 541 Build: "1", 542 }, 543 { 544 Name: "Dos", 545 Build: "2", 546 }, 547 { 548 Name: "San", 549 Build: "3", 550 }, 551 { 552 Name: "Four", 553 Build: "4", 554 }, 555 }, 556 }, 557 mod: now, 558 gen: 43, 559 features: FeatureFlags{ 560 AllowFuzzyFlakiness: true, 561 }, 562 expected: &summarypb.DashboardTabSummary{ 563 DashboardTabName: "foo-tab", 564 LastUpdateTimestamp: float64(now.Unix()), 565 OverallStatus: summarypb.DashboardTabSummary_ACCEPTABLE, 566 LatestGreen: "1", 567 Status: "Tab stats: 3 of 4 (75.0%) recent columns passed (3 of 4 or 75.0% cells)\nStatus info: Recent flakiness (25.0%) over valid columns is within configured acceptable level of 50.0%.", 568 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{ 569 CompletedColumns: 4, 570 PassingColumns: 3, 571 }, 572 }, 573 }, 574 { 575 name: "fuzzy flakiness configured but not allowed", 576 tab: &configpb.DashboardTab{ 577 Name: "foo-tab", 578 TestGroupName: "foo-group", 579 NumColumnsRecent: 4, 580 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 581 MaxAcceptableFlakiness: 50.0, 582 }, 583 }, 584 group: &configpb.TestGroup{}, 585 grid: &statepb.Grid{ 586 Rows: []*statepb.Row{ 587 { 588 Name: "test-1", 589 Results: []int32{int32(statuspb.TestStatus_PASS), 3, int32(statuspb.TestStatus_FAIL), 1}, 590 }, 591 }, 592 Columns: []*statepb.Column{ 593 { 594 Name: "Uno", 595 Build: "1", 596 }, 597 { 598 Name: "Dos", 599 Build: "2", 600 }, 601 { 602 Name: "Three", 603 Build: "3", 604 }, 605 { 606 Name: "Quattro", 607 Build: "4", 608 }, 609 }, 610 }, 611 mod: now, 612 gen: 43, 613 features: FeatureFlags{ 614 AllowFuzzyFlakiness: false, 615 }, 616 expected: &summarypb.DashboardTabSummary{ 617 DashboardTabName: "foo-tab", 618 LastUpdateTimestamp: float64(now.Unix()), 619 OverallStatus: summarypb.DashboardTabSummary_FLAKY, 620 LatestGreen: "1", 621 Status: "Tab stats: 3 of 4 (75.0%) recent columns passed (3 of 4 or 75.0% cells)", 622 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{ 623 CompletedColumns: 4, 624 PassingColumns: 3, 625 }, 626 }, 627 }, 628 { 629 name: "ignored columns configured and allowed", 630 tab: &configpb.DashboardTab{ 631 Name: "foo-tab", 632 TestGroupName: "foo-group", 633 NumColumnsRecent: 4, 634 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 635 IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{ 636 configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT, 637 configpb.DashboardTabStatusCustomizationOptions_CANCEL, 638 }, 639 }, 640 }, 641 group: &configpb.TestGroup{}, 642 grid: &statepb.Grid{ 643 Rows: []*statepb.Row{ 644 { 645 Name: "test-1", 646 Results: []int32{ 647 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1, 648 int32(statuspb.TestStatus_PASS), 3, 649 }, 650 }, 651 { 652 Name: "test-2", 653 Results: []int32{ 654 int32(statuspb.TestStatus_FAIL), 1, 655 int32(statuspb.TestStatus_CANCEL), 1, 656 int32(statuspb.TestStatus_PASS), 2, 657 }, 658 }, 659 }, 660 Columns: []*statepb.Column{ 661 { 662 Name: "Uno", 663 Build: "1", 664 }, 665 { 666 Name: "Dos", 667 Build: "2", 668 }, 669 { 670 Name: "San", 671 Build: "3", 672 }, 673 { 674 Name: "Chetyre", 675 Build: "4", 676 }, 677 }, 678 }, 679 mod: now, 680 gen: 44, 681 features: FeatureFlags{ 682 AllowIgnoredColumns: true, 683 }, 684 expected: &summarypb.DashboardTabSummary{ 685 DashboardTabName: "foo-tab", 686 LastUpdateTimestamp: float64(now.Unix()), 687 OverallStatus: summarypb.DashboardTabSummary_PASS, 688 LatestGreen: "3", 689 Status: "Tab stats: 2 of 4 (50.0%) recent columns passed (5 of 8 or 62.5% cells). 2 columns ignored", 690 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{ 691 CompletedColumns: 4, 692 PassingColumns: 2, 693 IgnoredColumns: 2, 694 }, 695 }, 696 }, 697 { 698 name: "ignored columns configured but not allowed", 699 tab: &configpb.DashboardTab{ 700 Name: "foo-tab", 701 TestGroupName: "foo-group", 702 NumColumnsRecent: 4, 703 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 704 IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{ 705 configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT, 706 configpb.DashboardTabStatusCustomizationOptions_CANCEL, 707 }, 708 }, 709 }, 710 group: &configpb.TestGroup{}, 711 grid: &statepb.Grid{ 712 Rows: []*statepb.Row{ 713 { 714 Name: "test-1", 715 Results: []int32{ 716 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1, 717 int32(statuspb.TestStatus_PASS), 3, 718 }, 719 }, 720 { 721 Name: "test-2", 722 Results: []int32{ 723 int32(statuspb.TestStatus_FAIL), 1, 724 int32(statuspb.TestStatus_CANCEL), 1, 725 int32(statuspb.TestStatus_PASS), 2, 726 }, 727 }, 728 }, 729 Columns: []*statepb.Column{ 730 { 731 Name: "Uno", 732 Build: "1", 733 }, 734 { 735 Name: "Dos", 736 Build: "2", 737 }, 738 { 739 Name: "San", 740 Build: "3", 741 }, 742 { 743 Name: "Chetyre", 744 Build: "4", 745 }, 746 }, 747 }, 748 mod: now, 749 gen: 44, 750 features: FeatureFlags{ 751 AllowIgnoredColumns: false, 752 }, 753 expected: &summarypb.DashboardTabSummary{ 754 DashboardTabName: "foo-tab", 755 LastUpdateTimestamp: float64(now.Unix()), 756 OverallStatus: summarypb.DashboardTabSummary_FLAKY, 757 LatestGreen: "3", 758 Status: "Tab stats: 3 of 4 (75.0%) recent columns passed (5 of 8 or 62.5% cells)", 759 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{ 760 CompletedColumns: 4, 761 PassingColumns: 3, 762 IgnoredColumns: 0, 763 }, 764 }, 765 }, 766 { 767 name: "min required runs configured and allowed", 768 tab: &configpb.DashboardTab{ 769 Name: "foo-tab", 770 TestGroupName: "foo-group", 771 NumColumnsRecent: 4, 772 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 773 MinAcceptableRuns: 5, 774 }, 775 }, 776 group: &configpb.TestGroup{}, 777 grid: &statepb.Grid{ 778 Rows: []*statepb.Row{ 779 { 780 Name: "test-1", 781 Results: []int32{ 782 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1, 783 int32(statuspb.TestStatus_PASS), 3, 784 }, 785 }, 786 { 787 Name: "test-2", 788 Results: []int32{ 789 int32(statuspb.TestStatus_FAIL), 1, 790 int32(statuspb.TestStatus_CANCEL), 1, 791 int32(statuspb.TestStatus_PASS), 2, 792 }, 793 }, 794 }, 795 Columns: []*statepb.Column{ 796 { 797 Name: "Uno", 798 Build: "1", 799 }, 800 { 801 Name: "Dos", 802 Build: "2", 803 }, 804 { 805 Name: "San", 806 Build: "3", 807 }, 808 { 809 Name: "Chetyre", 810 Build: "4", 811 }, 812 }, 813 }, 814 mod: now, 815 gen: 45, 816 features: FeatureFlags{ 817 AllowMinNumberOfRuns: true, 818 }, 819 expected: &summarypb.DashboardTabSummary{ 820 DashboardTabName: "foo-tab", 821 LastUpdateTimestamp: float64(now.Unix()), 822 OverallStatus: summarypb.DashboardTabSummary_PENDING, 823 LatestGreen: "3", 824 Status: "Tab stats: 3 of 4 (75.0%) recent columns passed (5 of 8 or 62.5% cells)\nStatus info: Not enough runs", 825 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{ 826 CompletedColumns: 4, 827 PassingColumns: 3, 828 IgnoredColumns: 0, 829 }, 830 }, 831 }, 832 { 833 name: "min required runs configured but not allowed", 834 tab: &configpb.DashboardTab{ 835 Name: "foo-tab", 836 TestGroupName: "foo-group", 837 NumColumnsRecent: 4, 838 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 839 MinAcceptableRuns: 5, 840 }, 841 }, 842 group: &configpb.TestGroup{}, 843 grid: &statepb.Grid{ 844 Rows: []*statepb.Row{ 845 { 846 Name: "test-1", 847 Results: []int32{ 848 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1, 849 int32(statuspb.TestStatus_PASS), 3, 850 }, 851 }, 852 { 853 Name: "test-2", 854 Results: []int32{ 855 int32(statuspb.TestStatus_FAIL), 1, 856 int32(statuspb.TestStatus_CANCEL), 1, 857 int32(statuspb.TestStatus_PASS), 2, 858 }, 859 }, 860 }, 861 Columns: []*statepb.Column{ 862 { 863 Name: "Uno", 864 Build: "1", 865 }, 866 { 867 Name: "Dos", 868 Build: "2", 869 }, 870 { 871 Name: "San", 872 Build: "3", 873 }, 874 { 875 Name: "Chetyre", 876 Build: "4", 877 }, 878 }, 879 }, 880 mod: now, 881 gen: 45, 882 features: FeatureFlags{ 883 AllowMinNumberOfRuns: false, 884 }, 885 expected: &summarypb.DashboardTabSummary{ 886 DashboardTabName: "foo-tab", 887 LastUpdateTimestamp: float64(now.Unix()), 888 OverallStatus: summarypb.DashboardTabSummary_FLAKY, 889 LatestGreen: "3", 890 Status: "Tab stats: 3 of 4 (75.0%) recent columns passed (5 of 8 or 62.5% cells)", 891 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{ 892 CompletedColumns: 4, 893 PassingColumns: 3, 894 IgnoredColumns: 0, 895 }, 896 }, 897 }, 898 { 899 name: "not enough runs after ignoring", 900 tab: &configpb.DashboardTab{ 901 Name: "foo-tab", 902 TestGroupName: "foo-group", 903 NumColumnsRecent: 4, 904 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 905 MinAcceptableRuns: 3, 906 MaxAcceptableFlakiness: 50.0, 907 IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{ 908 configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT, 909 configpb.DashboardTabStatusCustomizationOptions_BLOCKED, 910 }, 911 }, 912 }, 913 group: &configpb.TestGroup{}, 914 grid: &statepb.Grid{ 915 Rows: []*statepb.Row{ 916 { 917 Name: "test-1", 918 Results: []int32{ 919 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1, 920 int32(statuspb.TestStatus_PASS), 3, 921 }, 922 }, 923 { 924 Name: "test-2", 925 Results: []int32{ 926 int32(statuspb.TestStatus_FAIL), 1, 927 int32(statuspb.TestStatus_BLOCKED), 1, 928 int32(statuspb.TestStatus_PASS), 2, 929 }, 930 }, 931 }, 932 Columns: []*statepb.Column{ 933 { 934 Name: "Uno", 935 Build: "1", 936 }, 937 { 938 Name: "Dos", 939 Build: "2", 940 }, 941 { 942 Name: "San", 943 Build: "3", 944 }, 945 { 946 Name: "Chetyre", 947 Build: "4", 948 }, 949 }, 950 }, 951 mod: now, 952 gen: 45, 953 features: FeatureFlags{ 954 AllowMinNumberOfRuns: true, 955 AllowFuzzyFlakiness: true, 956 AllowIgnoredColumns: true, 957 }, 958 expected: &summarypb.DashboardTabSummary{ 959 DashboardTabName: "foo-tab", 960 LastUpdateTimestamp: float64(now.Unix()), 961 OverallStatus: summarypb.DashboardTabSummary_PENDING, 962 LatestGreen: "3", 963 Status: "Tab stats: 2 of 4 (50.0%) recent columns passed (5 of 8 or 62.5% cells). 2 columns ignored\nStatus info: Not enough runs", 964 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{ 965 CompletedColumns: 4, 966 PassingColumns: 2, 967 IgnoredColumns: 2, 968 }, 969 }, 970 }, 971 { 972 name: "acceptably flaky after ignoring", 973 tab: &configpb.DashboardTab{ 974 Name: "foo-tab", 975 TestGroupName: "foo-group", 976 NumColumnsRecent: 4, 977 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 978 MaxAcceptableFlakiness: 35.0, 979 IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{ 980 configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT, 981 configpb.DashboardTabStatusCustomizationOptions_BLOCKED, 982 }, 983 }, 984 }, 985 group: &configpb.TestGroup{}, 986 grid: &statepb.Grid{ 987 Rows: []*statepb.Row{ 988 { 989 Name: "test-1", 990 Results: []int32{ 991 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1, 992 int32(statuspb.TestStatus_PASS), 3, 993 }, 994 }, 995 { 996 Name: "test-2", 997 Results: []int32{ 998 int32(statuspb.TestStatus_FAIL), 2, 999 int32(statuspb.TestStatus_PASS), 2, 1000 }, 1001 }, 1002 }, 1003 Columns: []*statepb.Column{ 1004 { 1005 Name: "Uno", 1006 Build: "1", 1007 }, 1008 { 1009 Name: "Dos", 1010 Build: "2", 1011 }, 1012 { 1013 Name: "San", 1014 Build: "3", 1015 }, 1016 { 1017 Name: "Chetyre", 1018 Build: "4", 1019 }, 1020 }, 1021 }, 1022 mod: now, 1023 gen: 45, 1024 features: FeatureFlags{ 1025 AllowMinNumberOfRuns: true, 1026 AllowFuzzyFlakiness: true, 1027 AllowIgnoredColumns: true, 1028 }, 1029 expected: &summarypb.DashboardTabSummary{ 1030 DashboardTabName: "foo-tab", 1031 LastUpdateTimestamp: float64(now.Unix()), 1032 OverallStatus: summarypb.DashboardTabSummary_ACCEPTABLE, 1033 LatestGreen: "3", 1034 Status: "Tab stats: 2 of 4 (50.0%) recent columns passed (5 of 8 or 62.5% cells). 1 columns ignored\nStatus info: Recent flakiness (33.3%) over valid columns is within configured acceptable level of 35.0%.", 1035 SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{ 1036 CompletedColumns: 4, 1037 PassingColumns: 2, 1038 IgnoredColumns: 1, 1039 }, 1040 }, 1041 }, 1042 { 1043 name: "missing grid returns a blank summary", 1044 tab: &configpb.DashboardTab{ 1045 Name: "you know", 1046 }, 1047 group: &configpb.TestGroup{}, 1048 gridError: fmt.Errorf("oh yeah: %w", storage.ErrObjectNotExist), 1049 err: true, 1050 }, 1051 } 1052 1053 for _, tc := range cases { 1054 t.Run(tc.name, func(t *testing.T) { 1055 if tc.tab == nil { 1056 tc.tab = &configpb.DashboardTab{} 1057 } 1058 reader := func(_ context.Context) (io.ReadCloser, time.Time, int64, error) { 1059 if tc.gridError != nil { 1060 return nil, time.Time{}, 0, tc.gridError 1061 } 1062 return ioutil.NopCloser(bytes.NewBuffer(compress(gridBuf(tc.grid)))), tc.mod, tc.gen, nil 1063 } 1064 actual, err := updateTab(context.Background(), tc.tab, tc.group, reader, tc.features) 1065 switch { 1066 case err != nil: 1067 if !tc.err { 1068 t.Errorf("unexpected error: %v", err) 1069 } 1070 case tc.err: 1071 t.Errorf("failed to receive expected error") 1072 case !proto.Equal(actual, tc.expected): 1073 t.Errorf("actual summary: %s != expected %s", actual, tc.expected) 1074 } 1075 }) 1076 } 1077 } 1078 1079 func TestReadGrid(t *testing.T) { 1080 cases := []struct { 1081 name string 1082 reader io.Reader 1083 err error 1084 expectedGrid *statepb.Grid 1085 expectErr bool 1086 }{ 1087 { 1088 name: "error opening returns error", 1089 err: errors.New("open failed"), 1090 expectErr: true, 1091 }, 1092 { 1093 name: "return error when state is not compressed", 1094 reader: bytes.NewBuffer(gridBuf(&statepb.Grid{ 1095 LastTimeUpdated: 444, 1096 })), 1097 expectErr: true, 1098 }, 1099 { 1100 name: "return error when compressed object is not a grid proto", 1101 reader: bytes.NewBuffer(compress([]byte("hello"))), 1102 expectErr: true, 1103 }, 1104 { 1105 name: "return error when compressed proto is truncated", 1106 reader: bytes.NewBuffer(compress(gridBuf(&statepb.Grid{ 1107 Columns: []*statepb.Column{ 1108 { 1109 Build: "really long info", 1110 Name: "weeee", 1111 HotlistIds: "super exciting", 1112 }, 1113 }, 1114 LastTimeUpdated: 555, 1115 }))[:10]), 1116 expectErr: true, 1117 }, 1118 { 1119 name: "successfully parse compressed grid", 1120 reader: bytes.NewBuffer(compress(gridBuf(&statepb.Grid{ 1121 LastTimeUpdated: 555, 1122 }))), 1123 expectedGrid: &statepb.Grid{ 1124 LastTimeUpdated: 555, 1125 }, 1126 }, 1127 } 1128 1129 for _, tc := range cases { 1130 now := time.Now() 1131 t.Run(tc.name, func(t *testing.T) { 1132 const gen = 42 1133 reader := func(_ context.Context) (io.ReadCloser, time.Time, int64, error) { 1134 if tc.err != nil { 1135 return nil, time.Time{}, 0, tc.err 1136 } 1137 return ioutil.NopCloser(tc.reader), now, gen, nil 1138 } 1139 1140 actualGrid, aT, aGen, err := readGrid(context.Background(), reader) 1141 1142 switch { 1143 case err != nil: 1144 if !tc.expectErr { 1145 t.Errorf("unexpected error: %v", err) 1146 } 1147 case tc.expectErr: 1148 t.Error("failed to receive expected error") 1149 case !proto.Equal(actualGrid, tc.expectedGrid): 1150 t.Errorf("actual state: %#v != expected %#v", actualGrid, tc.expectedGrid) 1151 case !now.Equal(aT): 1152 t.Errorf("actual modified: %v != expected %v", aT, now) 1153 case aGen != gen: 1154 t.Errorf("actual generation: %d != expected %d", aGen, gen) 1155 } 1156 }) 1157 } 1158 } 1159 1160 func TestRecentColumns(t *testing.T) { 1161 cases := []struct { 1162 name string 1163 tab int32 1164 group int32 1165 expected int 1166 }{ 1167 { 1168 name: "prefer tab over group", 1169 tab: 1, 1170 group: 2, 1171 expected: 1, 1172 }, 1173 { 1174 name: "use group if tab is empty", 1175 group: 9, 1176 expected: 9, 1177 }, 1178 { 1179 name: "use default when both are empty", 1180 expected: 5, 1181 }, 1182 } 1183 1184 for _, tc := range cases { 1185 t.Run(tc.name, func(t *testing.T) { 1186 tabCfg := &configpb.DashboardTab{ 1187 NumColumnsRecent: tc.tab, 1188 } 1189 groupCfg := &configpb.TestGroup{ 1190 NumColumnsRecent: tc.group, 1191 } 1192 if actual := recentColumns(tabCfg, groupCfg); actual != tc.expected { 1193 t.Errorf("actual %d != expected %d", actual, tc.expected) 1194 } 1195 }) 1196 } 1197 } 1198 1199 func TestAllLinkedIssues(t *testing.T) { 1200 cases := []struct { 1201 name string 1202 rows []*statepb.Row 1203 want []string 1204 }{ 1205 { 1206 name: "no rows", 1207 rows: []*statepb.Row{}, 1208 want: []string{}, 1209 }, 1210 { 1211 name: "rows with no linked issues", 1212 rows: []*statepb.Row{ 1213 { 1214 Name: "test-1", 1215 Results: []int32{int32(statuspb.TestStatus_PASS), 10}, 1216 }, 1217 { 1218 Name: "test-2", 1219 Results: []int32{int32(statuspb.TestStatus_PASS), 10}, 1220 }, 1221 { 1222 Name: "test-3", 1223 Results: []int32{int32(statuspb.TestStatus_PASS), 10}, 1224 }, 1225 }, 1226 want: []string{}, 1227 }, 1228 { 1229 name: "multiple linked issues", 1230 rows: []*statepb.Row{ 1231 { 1232 Name: "test-1", 1233 Results: []int32{int32(statuspb.TestStatus_PASS), 10}, 1234 Issues: []string{"1", "2"}, 1235 }, 1236 { 1237 Name: "test-2", 1238 Results: []int32{int32(statuspb.TestStatus_PASS), 10}, 1239 Issues: []string{"5"}, 1240 }, 1241 { 1242 Name: "test-3", 1243 Results: []int32{int32(statuspb.TestStatus_PASS), 10}, 1244 Issues: []string{"10", "7"}, 1245 }, 1246 }, 1247 want: []string{"1", "2", "5", "7", "10"}, 1248 }, 1249 { 1250 name: "multiple linked issues with duplicates", 1251 rows: []*statepb.Row{ 1252 { 1253 Name: "test-1", 1254 Results: []int32{int32(statuspb.TestStatus_PASS), 10}, 1255 Issues: []string{"1", "2"}, 1256 }, 1257 { 1258 Name: "test-2", 1259 Results: []int32{int32(statuspb.TestStatus_PASS), 10}, 1260 Issues: []string{"2", "3"}, 1261 }, 1262 }, 1263 want: []string{"1", "2", "3"}, 1264 }, 1265 } 1266 1267 for _, tc := range cases { 1268 t.Run(tc.name, func(t *testing.T) { 1269 got := allLinkedIssues(tc.rows) 1270 sort.Strings(got) 1271 strSort := cmpopts.SortSlices(func(a, b string) bool { return a < b }) 1272 if diff := cmp.Diff(tc.want, got, strSort); diff != "" { 1273 t.Errorf("allLinkedIssues() unexpected diff (-want +got): %s", diff) 1274 } 1275 }) 1276 } 1277 } 1278 1279 func TestFirstFilled(t *testing.T) { 1280 cases := []struct { 1281 name string 1282 values []int32 1283 expected int 1284 }{ 1285 { 1286 name: "zero by default", 1287 }, 1288 { 1289 name: "first non-zero value", 1290 values: []int32{0, 1, 2}, 1291 expected: 1, 1292 }, 1293 } 1294 1295 for _, tc := range cases { 1296 t.Run(tc.name, func(t *testing.T) { 1297 if actual := firstFilled(tc.values...); actual != tc.expected { 1298 t.Errorf("actual %d != expected %d", actual, tc.expected) 1299 } 1300 }) 1301 } 1302 } 1303 1304 func TestFilterMethods(t *testing.T) { 1305 cases := []struct { 1306 name string 1307 rows []*statepb.Row 1308 recent int 1309 expected []*statepb.Row 1310 err bool 1311 }{ 1312 { 1313 name: "tolerates nil inputs", 1314 }, 1315 { 1316 name: "basically works", 1317 rows: []*statepb.Row{ 1318 { 1319 Name: "okay", 1320 Id: "cool", 1321 }, 1322 }, 1323 expected: []*statepb.Row{ 1324 { 1325 Name: "okay", 1326 Id: "cool", 1327 }, 1328 }, 1329 }, 1330 { 1331 name: "exclude all test methods", 1332 rows: []*statepb.Row{ 1333 { 1334 Name: "test-1", 1335 Id: "test-1", 1336 }, 1337 { 1338 Name: "method-1", 1339 Id: "test-1@TESTGRID@method-1", 1340 }, 1341 { 1342 Name: "method-2", 1343 Id: "test-1@TESTGRID@method-2", 1344 }, 1345 { 1346 Name: "test-2", 1347 Id: "test-2", 1348 }, 1349 { 1350 Name: "test-2@TESTGRID@method-1", 1351 Id: "method-1", 1352 }, 1353 }, 1354 expected: []*statepb.Row{ 1355 { 1356 Name: "test-1", 1357 Id: "test-1", 1358 }, 1359 { 1360 Name: "test-2", 1361 Id: "test-2", 1362 }, 1363 }, 1364 }, 1365 } 1366 1367 for _, tc := range cases { 1368 t.Run(tc.name, func(t *testing.T) { 1369 for _, r := range tc.rows { 1370 if r.Results == nil { 1371 r.Results = []int32{int32(statuspb.TestStatus_PASS), 100} 1372 } 1373 } 1374 for _, r := range tc.expected { 1375 if r.Results == nil { 1376 r.Results = []int32{int32(statuspb.TestStatus_PASS), 100} 1377 } 1378 } 1379 actual := filterMethods(tc.rows) 1380 1381 if !cmp.Equal(actual, tc.expected, protocmp.Transform()) { 1382 t.Errorf("%s != expected %s", actual, tc.expected) 1383 } 1384 }) 1385 } 1386 } 1387 1388 func TestRecentRows(t *testing.T) { 1389 const recent = 10 1390 cases := []struct { 1391 name string 1392 rows []*statepb.Row 1393 expected []string 1394 }{ 1395 { 1396 name: "basically works", 1397 }, 1398 { 1399 name: "skip row with nil results", 1400 rows: []*statepb.Row{ 1401 { 1402 Name: "include", 1403 Results: []int32{int32(statuspb.TestStatus_PASS), recent}, 1404 }, 1405 { 1406 Name: "skip-nil-results", 1407 }, 1408 }, 1409 expected: []string{"include"}, 1410 }, 1411 { 1412 name: "skip row with no recent results", 1413 rows: []*statepb.Row{ 1414 { 1415 Name: "include", 1416 Results: []int32{int32(statuspb.TestStatus_PASS), recent}, 1417 }, 1418 { 1419 Name: "skip-this-one-with-no-recent-results", 1420 Results: []int32{int32(statuspb.TestStatus_NO_RESULT), recent}, 1421 }, 1422 }, 1423 expected: []string{"include"}, 1424 }, 1425 { 1426 name: "include rows missing some recent results", 1427 rows: []*statepb.Row{ 1428 { 1429 Name: "head skips", 1430 Results: []int32{ 1431 int32(statuspb.TestStatus_NO_RESULT), recent - 1, 1432 int32(statuspb.TestStatus_PASS_WITH_SKIPS), recent, 1433 }, 1434 }, 1435 { 1436 Name: "tail skips", 1437 Results: []int32{ 1438 int32(statuspb.TestStatus_FLAKY), recent - 1, 1439 int32(statuspb.TestStatus_NO_RESULT), recent, 1440 }, 1441 }, 1442 { 1443 Name: "middle skips", 1444 Results: []int32{ 1445 int32(statuspb.TestStatus_FAIL), 1, 1446 int32(statuspb.TestStatus_NO_RESULT), recent - 2, 1447 int32(statuspb.TestStatus_PASS), 1, 1448 }, 1449 }, 1450 }, 1451 expected: []string{ 1452 "head skips", 1453 "tail skips", 1454 "middle skips", 1455 }, 1456 }, 1457 } 1458 1459 for _, tc := range cases { 1460 t.Run(tc.name, func(t *testing.T) { 1461 actualRows := recentRows(tc.rows, recent) 1462 1463 var actual []string 1464 for _, r := range actualRows { 1465 actual = append(actual, r.Name) 1466 } 1467 if !cmp.Equal(actual, tc.expected, protocmp.Transform()) { 1468 t.Errorf("%s != expected %s", actual, tc.expected) 1469 } 1470 }) 1471 } 1472 } 1473 1474 func TestLatestRun(t *testing.T) { 1475 cases := []struct { 1476 name string 1477 cols []*statepb.Column 1478 expectedTime time.Time 1479 expectedSecs int64 1480 }{ 1481 { 1482 name: "basically works", 1483 }, 1484 { 1485 name: "zero started returns zero time", 1486 cols: []*statepb.Column{ 1487 {}, 1488 }, 1489 }, 1490 { 1491 name: "return first time in unix", 1492 cols: []*statepb.Column{ 1493 { 1494 Started: 333333, 1495 }, 1496 { 1497 Started: 222222, 1498 }, 1499 }, 1500 expectedTime: time.Unix(333, 333000000), 1501 expectedSecs: 333, 1502 }, 1503 } 1504 1505 for _, tc := range cases { 1506 t.Run(tc.name, func(t *testing.T) { 1507 when, s := latestRun(tc.cols) 1508 if !when.Equal(tc.expectedTime) { 1509 t.Errorf("time %v != expected %v", when, tc.expectedTime) 1510 } 1511 if s != tc.expectedSecs { 1512 t.Errorf("seconds %d != expected %d", s, tc.expectedSecs) 1513 } 1514 }) 1515 } 1516 } 1517 1518 func TestStaleAlert(t *testing.T) { 1519 cases := []struct { 1520 name string 1521 mod time.Time 1522 ran time.Time 1523 dur time.Duration 1524 rows int 1525 alert bool 1526 }{ 1527 { 1528 name: "basically works", 1529 mod: time.Now().Add(-5 * time.Minute), 1530 ran: time.Now().Add(-10 * time.Minute), 1531 dur: time.Hour, 1532 rows: 10, 1533 }, 1534 { 1535 name: "unmodified alerts", 1536 mod: time.Now().Add(-5 * time.Hour), 1537 ran: time.Now(), 1538 dur: time.Hour, 1539 rows: 10, 1540 alert: true, 1541 }, 1542 { 1543 name: "no recent runs alerts", 1544 mod: time.Now(), 1545 ran: time.Now().Add(-5 * time.Hour), 1546 dur: time.Hour, 1547 rows: 10, 1548 alert: true, 1549 }, 1550 { 1551 name: "no runs alerts", 1552 mod: time.Now(), 1553 dur: time.Hour, 1554 rows: 10, 1555 alert: true, 1556 }, 1557 { 1558 name: "no rows alerts", 1559 mod: time.Now().Add(-5 * time.Minute), 1560 ran: time.Now().Add(-10 * time.Minute), 1561 dur: time.Hour, 1562 rows: 0, 1563 alert: true, 1564 }, 1565 { 1566 name: "no runs w/ stale hours not configured does not alert", 1567 mod: time.Now(), 1568 }, 1569 { 1570 name: "no state w/ stale hours not configured alerts", 1571 alert: true, 1572 }, 1573 } 1574 1575 for _, tc := range cases { 1576 t.Run(tc.name, func(t *testing.T) { 1577 actual := staleAlert(tc.mod, tc.ran, tc.dur, tc.rows) 1578 if actual != "" && !tc.alert { 1579 t.Errorf("unexpected stale alert: %s", actual) 1580 } 1581 if actual == "" && tc.alert { 1582 t.Errorf("failed to create a stale alert") 1583 } 1584 }) 1585 } 1586 } 1587 1588 func TestFailingTestSummaries(t *testing.T) { 1589 defaultTemplate := &configpb.LinkTemplate{ 1590 Url: "http://test.com/view/<workflow-name>/<workflow-id>", 1591 Options: []*configpb.LinkOptionsTemplate{ 1592 { 1593 Key: "test", 1594 Value: "<test-name>@<test-id>", 1595 }, 1596 { 1597 Key: "path", 1598 Value: "<encode:<gcs_prefix>>", 1599 }, 1600 }, 1601 } 1602 defaultGcsPrefix := "my-bucket/logs/cool-job" 1603 defaultColumnHeader := []*configpb.TestGroup_ColumnHeader{ 1604 { 1605 Property: "foo", 1606 }, 1607 { 1608 Label: "hello", 1609 }, 1610 } 1611 cases := []struct { 1612 name string 1613 template *configpb.LinkTemplate 1614 gcsPrefix string 1615 columnHeader []*configpb.TestGroup_ColumnHeader 1616 rows []*statepb.Row 1617 expected []*summarypb.FailingTestSummary 1618 }{ 1619 { 1620 name: "do not alert by default", 1621 template: defaultTemplate, 1622 gcsPrefix: defaultGcsPrefix, 1623 columnHeader: defaultColumnHeader, 1624 rows: []*statepb.Row{ 1625 {}, 1626 {}, 1627 }, 1628 }, 1629 { 1630 name: "alert when rows have alerts", 1631 template: defaultTemplate, 1632 gcsPrefix: defaultGcsPrefix, 1633 columnHeader: []*configpb.TestGroup_ColumnHeader{ 1634 { 1635 Property: "foo", 1636 }, 1637 }, 1638 rows: []*statepb.Row{ 1639 {}, 1640 { 1641 Name: "foo-name", 1642 Id: "foo-target", 1643 Issues: []string{"1234", "5678"}, 1644 AlertInfo: &statepb.AlertInfo{ 1645 FailBuildId: "bad", 1646 LatestFailBuildId: "still-bad", 1647 PassBuildId: "good", 1648 FailCount: 6, 1649 BuildLink: "to the past", 1650 BuildLinkText: "hyrule", 1651 BuildUrlText: "of sandwich", 1652 FailureMessage: "pop tart", 1653 Properties: map[string]string{ 1654 "ham": "eggs", 1655 }, 1656 CustomColumnHeaders: map[string]string{ 1657 "foo": "bar", 1658 }, 1659 HotlistIds: []string{}, 1660 }, 1661 }, 1662 {}, 1663 { 1664 Name: "bar-name", 1665 Id: "bar-target", 1666 Issues: []string{"1234"}, 1667 AlertInfo: &statepb.AlertInfo{ 1668 FailBuildId: "fbi", 1669 LatestFailBuildId: "lfbi", 1670 PassBuildId: "pbi", 1671 FailTestId: "819283y823-1232813", 1672 LatestFailTestId: "920394z934-2343924", 1673 FailCount: 1, 1674 BuildLink: "bl", 1675 BuildLinkText: "blt", 1676 BuildUrlText: "but", 1677 FailureMessage: "fm", 1678 Properties: map[string]string{ 1679 "foo": "bar", 1680 "hello": "lots", 1681 }, 1682 CustomColumnHeaders: map[string]string{ 1683 "foo": "notbar", 1684 }, 1685 HotlistIds: []string{"111", "222"}, 1686 }, 1687 }, 1688 {}, 1689 }, 1690 expected: []*summarypb.FailingTestSummary{ 1691 { 1692 DisplayName: "foo-name", 1693 TestName: "foo-target", 1694 FailBuildId: "bad", 1695 LatestFailBuildId: "still-bad", 1696 PassBuildId: "good", 1697 FailCount: 6, 1698 BuildLink: "to the past", 1699 BuildLinkText: "hyrule", 1700 BuildUrlText: "of sandwich", 1701 FailureMessage: "pop tart", 1702 FailTestLink: " foo-target", 1703 LatestFailTestLink: " foo-target", 1704 LinkedBugs: []string{"1234", "5678"}, 1705 Properties: map[string]string{ 1706 "ham": "eggs", 1707 }, 1708 CustomColumnHeaders: map[string]string{ 1709 "foo": "bar", 1710 }, 1711 HotlistIds: []string{}, 1712 }, 1713 { 1714 DisplayName: "bar-name", 1715 TestName: "bar-target", 1716 FailBuildId: "fbi", 1717 LatestFailBuildId: "lfbi", 1718 PassBuildId: "pbi", 1719 FailCount: 1, 1720 BuildLink: "bl", 1721 BuildLinkText: "blt", 1722 BuildUrlText: "but", 1723 FailureMessage: "fm", 1724 FailTestLink: "819283y823-1232813 bar-target", 1725 LatestFailTestLink: "920394z934-2343924 bar-target", 1726 LinkedBugs: []string{"1234"}, 1727 Properties: map[string]string{ 1728 "foo": "bar", 1729 "hello": "lots", 1730 }, 1731 CustomColumnHeaders: map[string]string{ 1732 "foo": "notbar", 1733 }, 1734 HotlistIds: []string{"111", "222"}, 1735 }, 1736 }, 1737 }, 1738 } 1739 1740 for _, tc := range cases { 1741 t.Run(tc.name, func(t *testing.T) { 1742 actual := failingTestSummaries(tc.rows, tc.template, tc.gcsPrefix, tc.columnHeader) 1743 if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" { 1744 t.Errorf("failingTestSummaries() (-want, +got): %s", diff) 1745 } 1746 }) 1747 } 1748 } 1749 1750 func TestOverallStatus(t *testing.T) { 1751 cases := []struct { 1752 name string 1753 rows []*statepb.Row 1754 recent int 1755 stale string 1756 broken bool 1757 alerts bool 1758 features FeatureFlags 1759 colCells gridStats 1760 opts *configpb.DashboardTabStatusCustomizationOptions 1761 expected summarypb.DashboardTabSummary_TabStatus 1762 }{ 1763 { 1764 name: "unknown by default", 1765 expected: summarypb.DashboardTabSummary_UNKNOWN, 1766 }, 1767 { 1768 name: "stale joke results in stale summary", 1769 stale: "joke", 1770 expected: summarypb.DashboardTabSummary_STALE, 1771 }, 1772 { 1773 name: "alerts result in failure", 1774 alerts: true, 1775 expected: summarypb.DashboardTabSummary_FAIL, 1776 }, 1777 { 1778 name: "prefer stale over failure", 1779 stale: "potato chip", 1780 alerts: true, 1781 expected: summarypb.DashboardTabSummary_STALE, 1782 }, 1783 { 1784 name: "completed results result in pass", 1785 recent: 1, 1786 rows: []*statepb.Row{ 1787 { 1788 Results: []int32{int32(statuspb.TestStatus_PASS), 1}, 1789 }, 1790 }, 1791 expected: summarypb.DashboardTabSummary_PASS, 1792 }, 1793 { 1794 name: "non-passing results without an alert results in flaky", 1795 recent: 1, 1796 rows: []*statepb.Row{ 1797 { 1798 Results: []int32{int32(statuspb.TestStatus_FAIL), 1}, 1799 }, 1800 }, 1801 expected: summarypb.DashboardTabSummary_FLAKY, 1802 }, 1803 { 1804 name: "incomplete passing results", // ignore them 1805 recent: 5, 1806 rows: []*statepb.Row{ 1807 { 1808 Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 1}, 1809 }, 1810 { 1811 Results: []int32{int32(statuspb.TestStatus_PASS), 3}, 1812 }, 1813 { 1814 Results: []int32{int32(statuspb.TestStatus_RUNNING), 2}, 1815 }, 1816 { 1817 Results: []int32{int32(statuspb.TestStatus_PASS), 2}, 1818 }, 1819 }, 1820 expected: summarypb.DashboardTabSummary_PASS, 1821 }, 1822 { 1823 name: "incomplete flaky results", // ignore them 1824 recent: 5, 1825 rows: []*statepb.Row{ 1826 { 1827 Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 1}, 1828 }, 1829 { 1830 Results: []int32{int32(statuspb.TestStatus_PASS), 3}, 1831 }, 1832 { 1833 Results: []int32{int32(statuspb.TestStatus_RUNNING), 2}, 1834 }, 1835 { 1836 Results: []int32{int32(statuspb.TestStatus_FAIL), 2}, 1837 }, 1838 }, 1839 expected: summarypb.DashboardTabSummary_FLAKY, 1840 }, 1841 { 1842 name: "ignore old failures", 1843 recent: 1, 1844 rows: []*statepb.Row{ 1845 { 1846 Results: []int32{ 1847 int32(statuspb.TestStatus_PASS), 3, 1848 int32(statuspb.TestStatus_FAIL), 5, 1849 }, 1850 }, 1851 }, 1852 expected: summarypb.DashboardTabSummary_PASS, 1853 }, 1854 { 1855 name: "dropped columns", // should not impact status 1856 recent: 1, 1857 rows: []*statepb.Row{ 1858 { 1859 Name: "current", 1860 Results: []int32{ 1861 int32(statuspb.TestStatus_PASS), 2, 1862 }, 1863 }, 1864 { 1865 Name: "ignore dropped", 1866 Results: []int32{ 1867 int32(statuspb.TestStatus_NO_RESULT), 1, 1868 int32(statuspb.TestStatus_FAIL), 1, 1869 }, 1870 }, 1871 }, 1872 expected: summarypb.DashboardTabSummary_PASS, 1873 }, 1874 { 1875 name: "running", // do not count as recent 1876 recent: 1, 1877 rows: []*statepb.Row{ 1878 { 1879 Name: "pass", 1880 Results: []int32{ 1881 int32(statuspb.TestStatus_PASS), 2, 1882 }, 1883 }, 1884 { 1885 Name: "running", 1886 Results: []int32{ 1887 int32(statuspb.TestStatus_RUNNING), 1, 1888 int32(statuspb.TestStatus_PASS), 1, 1889 }, 1890 }, 1891 { 1892 Name: "flake", 1893 Results: []int32{ 1894 int32(statuspb.TestStatus_PASS), 1, 1895 int32(statuspb.TestStatus_FAIL), 1, 1896 }, 1897 }, 1898 }, 1899 expected: summarypb.DashboardTabSummary_FLAKY, 1900 }, 1901 { 1902 name: "partial results work", 1903 recent: 50, 1904 rows: []*statepb.Row{ 1905 { 1906 Results: []int32{int32(statuspb.TestStatus_PASS), 1}, 1907 }, 1908 }, 1909 expected: summarypb.DashboardTabSummary_PASS, 1910 }, 1911 { 1912 name: "coalesce passes", 1913 recent: 1, 1914 rows: []*statepb.Row{ 1915 { 1916 Results: []int32{int32(statuspb.TestStatus_PASS_WITH_SKIPS), 1}, 1917 }, 1918 }, 1919 expected: summarypb.DashboardTabSummary_PASS, 1920 }, 1921 { 1922 name: "broken cycle", 1923 recent: 1, 1924 rows: []*statepb.Row{ 1925 { 1926 Results: []int32{int32(statuspb.TestStatus_PASS_WITH_SKIPS), 1}, 1927 }, 1928 }, 1929 broken: true, 1930 expected: summarypb.DashboardTabSummary_BROKEN, 1931 }, 1932 { 1933 name: "more runs required but flag not enabled", 1934 recent: 4, 1935 rows: []*statepb.Row{ 1936 { 1937 Results: []int32{ 1938 int32(statuspb.TestStatus_PASS_WITH_SKIPS), 2, 1939 int32(statuspb.TestStatus_FAIL), 2, 1940 }, 1941 }, 1942 }, 1943 features: FeatureFlags{ 1944 AllowMinNumberOfRuns: false, 1945 }, 1946 colCells: gridStats{ 1947 ignoredCols: 2, 1948 completedCols: 4, 1949 }, 1950 opts: &configpb.DashboardTabStatusCustomizationOptions{ 1951 MinAcceptableRuns: 3, 1952 }, 1953 expected: summarypb.DashboardTabSummary_FLAKY, 1954 }, 1955 { 1956 name: "more runs required with flag enabled", 1957 recent: 4, 1958 rows: []*statepb.Row{ 1959 { 1960 Results: []int32{ 1961 int32(statuspb.TestStatus_PASS_WITH_SKIPS), 2, 1962 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 2, 1963 }, 1964 }, 1965 }, 1966 features: FeatureFlags{ 1967 AllowMinNumberOfRuns: true, 1968 }, 1969 colCells: gridStats{ 1970 ignoredCols: 2, 1971 completedCols: 4, 1972 }, 1973 opts: &configpb.DashboardTabStatusCustomizationOptions{ 1974 MinAcceptableRuns: 3, 1975 }, 1976 expected: summarypb.DashboardTabSummary_PENDING, 1977 }, 1978 { 1979 name: "neutral statuses ignored but flag not enabled", 1980 recent: 4, 1981 rows: []*statepb.Row{ 1982 { 1983 Results: []int32{ 1984 int32(statuspb.TestStatus_PASS), 2, 1985 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 2, 1986 }, 1987 }, 1988 }, 1989 features: FeatureFlags{ 1990 AllowIgnoredColumns: false, 1991 }, 1992 opts: &configpb.DashboardTabStatusCustomizationOptions{ 1993 IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{ 1994 configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT, 1995 configpb.DashboardTabStatusCustomizationOptions_CANCEL, 1996 }, 1997 }, 1998 expected: summarypb.DashboardTabSummary_FLAKY, 1999 }, 2000 { 2001 name: "neutral statuses ignored with flag enabled (passes ignored)", 2002 recent: 4, 2003 rows: []*statepb.Row{ 2004 { 2005 Name: "passes and aborts", 2006 Results: []int32{ 2007 int32(statuspb.TestStatus_PASS), 2, 2008 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 2, 2009 }, 2010 }, 2011 { 2012 Name: "passes only", 2013 Results: []int32{ 2014 int32(statuspb.TestStatus_PASS), 4, 2015 }, 2016 }, 2017 }, 2018 features: FeatureFlags{ 2019 AllowIgnoredColumns: true, 2020 }, 2021 opts: &configpb.DashboardTabStatusCustomizationOptions{ 2022 IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{ 2023 configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT, 2024 configpb.DashboardTabStatusCustomizationOptions_CANCEL, 2025 }, 2026 }, 2027 expected: summarypb.DashboardTabSummary_PASS, 2028 }, 2029 { 2030 name: "neutral statuses ignored with flag enabled (fails detected before ignores)", 2031 recent: 4, 2032 rows: []*statepb.Row{ 2033 { 2034 Name: "passes and aborts", 2035 Results: []int32{ 2036 int32(statuspb.TestStatus_PASS), 2, 2037 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 2, 2038 }, 2039 }, 2040 { 2041 Name: "passes and fails", 2042 Results: []int32{ 2043 int32(statuspb.TestStatus_FAIL), 1, 2044 int32(statuspb.TestStatus_PASS), 3, 2045 }, 2046 }, 2047 }, 2048 features: FeatureFlags{ 2049 AllowIgnoredColumns: true, 2050 }, 2051 opts: &configpb.DashboardTabStatusCustomizationOptions{ 2052 IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{ 2053 configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT, 2054 configpb.DashboardTabStatusCustomizationOptions_CANCEL, 2055 }, 2056 }, 2057 expected: summarypb.DashboardTabSummary_FLAKY, 2058 }, 2059 } 2060 2061 for _, tc := range cases { 2062 t.Run(tc.name, func(t *testing.T) { 2063 var alerts []*summarypb.FailingTestSummary 2064 if tc.alerts { 2065 alerts = append(alerts, &summarypb.FailingTestSummary{}) 2066 } 2067 2068 if actual := overallStatus(&statepb.Grid{Rows: tc.rows}, tc.recent, tc.stale, tc.broken, alerts, tc.features, tc.colCells, tc.opts); actual != tc.expected { 2069 t.Errorf("%s != expected %s", actual, tc.expected) 2070 } 2071 }) 2072 } 2073 } 2074 2075 func makeShim(v ...interface{}) []interface{} { 2076 return v 2077 } 2078 2079 func TestGridMetrics(t *testing.T) { 2080 cases := []struct { 2081 name string 2082 cols int 2083 rows []*statepb.Row 2084 recent int 2085 features FeatureFlags 2086 opts *configpb.DashboardTabStatusCustomizationOptions 2087 brokenThreshold float32 2088 expectedMetrics gridStats 2089 expectedBroken bool 2090 }{ 2091 { 2092 name: "no runs", 2093 }, 2094 { 2095 name: "what people want (greens)", 2096 cols: 2, 2097 rows: []*statepb.Row{ 2098 { 2099 Name: "green eggs", 2100 Results: []int32{int32(statuspb.TestStatus_PASS), 2}, 2101 }, 2102 { 2103 Name: "and ham", 2104 Results: []int32{int32(statuspb.TestStatus_PASS), 2}, 2105 }, 2106 }, 2107 recent: 2, 2108 expectedMetrics: gridStats{ 2109 passingCols: 2, 2110 completedCols: 2, 2111 passingCells: 4, 2112 filledCells: 4, 2113 }, 2114 }, 2115 { 2116 name: "red: i do not like them sam I am", 2117 cols: 2, 2118 rows: []*statepb.Row{ 2119 { 2120 Name: "not with a fox", 2121 Results: []int32{int32(statuspb.TestStatus_FAIL), 2}, 2122 }, 2123 { 2124 Name: "not in a box", 2125 Results: []int32{int32(statuspb.TestStatus_FLAKY), 2}, 2126 }, 2127 }, 2128 recent: 2, 2129 expectedMetrics: gridStats{ 2130 passingCols: 0, 2131 completedCols: 2, 2132 passingCells: 0, 2133 filledCells: 4, 2134 }, 2135 }, 2136 { 2137 name: "passing cells but no green columns", 2138 cols: 2, 2139 rows: []*statepb.Row{ 2140 { 2141 Name: "first doughnut is best", 2142 Results: []int32{ 2143 int32(statuspb.TestStatus_PASS), 1, 2144 int32(statuspb.TestStatus_FAIL), 1, 2145 }, 2146 }, 2147 { 2148 Name: "fine wine gets better", 2149 Results: []int32{ 2150 int32(statuspb.TestStatus_FAIL), 1, 2151 int32(statuspb.TestStatus_PASS), 1, 2152 }, 2153 }, 2154 }, 2155 recent: 2, 2156 expectedMetrics: gridStats{ 2157 passingCols: 0, 2158 completedCols: 2, 2159 passingCells: 2, 2160 filledCells: 4}, 2161 }, 2162 { 2163 name: "ignore overflow of claimed columns", 2164 cols: 100, 2165 recent: 50, 2166 rows: []*statepb.Row{ 2167 { 2168 Name: "a", 2169 Results: []int32{int32(statuspb.TestStatus_PASS), 3}, 2170 }, 2171 { 2172 Name: "b", 2173 Results: []int32{int32(statuspb.TestStatus_PASS), 3}, 2174 }, 2175 }, 2176 expectedMetrics: gridStats{ 2177 passingCols: 3, 2178 completedCols: 3, 2179 passingCells: 6, 2180 filledCells: 6, 2181 }, 2182 }, 2183 { 2184 name: "ignore bad row data", 2185 cols: 2, 2186 recent: 2, 2187 rows: []*statepb.Row{ 2188 { 2189 Name: "empty", 2190 }, 2191 { 2192 Name: "filled", 2193 Results: []int32{int32(statuspb.TestStatus_PASS), 2}, 2194 }, 2195 }, 2196 expectedMetrics: gridStats{ 2197 passingCols: 2, 2198 completedCols: 2, 2199 passingCells: 2, 2200 filledCells: 2, 2201 }, 2202 }, 2203 { 2204 name: "ignore non recent data", 2205 cols: 100, 2206 recent: 2, 2207 rows: []*statepb.Row{ 2208 { 2209 Name: "data", 2210 Results: []int32{int32(statuspb.TestStatus_PASS), 100}, 2211 }, 2212 }, 2213 expectedMetrics: gridStats{ 2214 passingCols: 2, 2215 completedCols: 2, 2216 passingCells: 2, 2217 filledCells: 2, 2218 }, 2219 }, 2220 { 2221 name: "no result cells do not alter column", 2222 cols: 3, 2223 recent: 3, 2224 rows: []*statepb.Row{ 2225 { 2226 Name: "always empty", 2227 Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 3}, 2228 }, 2229 { 2230 Name: "first empty", 2231 Results: []int32{ 2232 int32(statuspb.TestStatus_NO_RESULT), 1, 2233 int32(statuspb.TestStatus_PASS), 2, 2234 }, 2235 }, 2236 { 2237 Name: "always pass", 2238 Results: []int32{ 2239 int32(statuspb.TestStatus_PASS), 3, 2240 }, 2241 }, 2242 { 2243 Name: "empty, fail, pass", 2244 Results: []int32{ 2245 int32(statuspb.TestStatus_NO_RESULT), 1, 2246 int32(statuspb.TestStatus_FAIL), 1, 2247 int32(statuspb.TestStatus_PASS), 1, 2248 }, 2249 }, 2250 }, 2251 expectedMetrics: gridStats{ 2252 passingCols: 2, // pass, fail, pass 2253 completedCols: 3, 2254 passingCells: 6, 2255 filledCells: 7, 2256 }, 2257 }, 2258 { 2259 name: "not enough columns yet works just fine", 2260 cols: 4, 2261 recent: 50, 2262 rows: []*statepb.Row{ 2263 { 2264 Name: "four passes", 2265 Results: []int32{ 2266 int32(statuspb.TestStatus_PASS), 4, 2267 }, 2268 }, 2269 }, 2270 expectedMetrics: gridStats{ 2271 passingCols: 4, 2272 completedCols: 4, 2273 passingCells: 4, 2274 filledCells: 4, 2275 }, 2276 }, 2277 { 2278 name: "half passes and half fails", 2279 cols: 4, 2280 recent: 4, 2281 rows: []*statepb.Row{ 2282 { 2283 Name: "four passes", 2284 Results: []int32{ 2285 int32(statuspb.TestStatus_PASS), 4, 2286 }, 2287 }, 2288 { 2289 Name: "four fails", 2290 Results: []int32{ 2291 int32(statuspb.TestStatus_FAIL), 4, 2292 }, 2293 }, 2294 }, 2295 expectedMetrics: gridStats{ 2296 passingCols: 0, 2297 completedCols: 4, 2298 passingCells: 4, 2299 filledCells: 8, 2300 }, 2301 }, 2302 { 2303 name: "no result in every column", 2304 cols: 3, 2305 recent: 3, 2306 rows: []*statepb.Row{ 2307 { 2308 Name: "always empty", 2309 Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 3}, 2310 }, 2311 { 2312 Name: "first empty", 2313 Results: []int32{ 2314 int32(statuspb.TestStatus_NO_RESULT), 1, 2315 int32(statuspb.TestStatus_PASS), 2, 2316 }, 2317 }, 2318 { 2319 Name: "always empty", 2320 Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 3}, 2321 }, 2322 }, 2323 expectedMetrics: gridStats{ 2324 passingCols: 2, 2325 completedCols: 2, 2326 passingCells: 2, 2327 filledCells: 2, 2328 }, 2329 }, 2330 { 2331 name: "only no result", 2332 cols: 3, 2333 recent: 3, 2334 rows: []*statepb.Row{ 2335 { 2336 Name: "always empty", 2337 Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 3}, 2338 }, 2339 }, 2340 expectedMetrics: gridStats{ 2341 passingCols: 0, 2342 completedCols: 0, 2343 passingCells: 0, 2344 filledCells: 0, 2345 }, 2346 }, 2347 { 2348 name: "Pass with skips", 2349 cols: 3, 2350 recent: 3, 2351 rows: []*statepb.Row{ 2352 { 2353 Name: "always empty", 2354 Results: []int32{int32(statuspb.TestStatus_PASS_WITH_SKIPS), 3}, 2355 }, 2356 { 2357 Name: "all pass", 2358 Results: []int32{int32(statuspb.TestStatus_PASS), 3}, 2359 }, 2360 }, 2361 expectedMetrics: gridStats{ 2362 passingCols: 3, 2363 completedCols: 3, 2364 passingCells: 6, 2365 filledCells: 6, 2366 }, 2367 }, 2368 { 2369 name: "Pass with errors", 2370 cols: 3, 2371 recent: 3, 2372 rows: []*statepb.Row{ 2373 { 2374 Name: "always empty", 2375 Results: []int32{int32(statuspb.TestStatus_PASS_WITH_ERRORS), 3}, 2376 }, 2377 { 2378 Name: "all pass", 2379 Results: []int32{int32(statuspb.TestStatus_PASS), 3}, 2380 }, 2381 }, 2382 expectedMetrics: gridStats{ 2383 passingCols: 3, 2384 completedCols: 3, 2385 passingCells: 6, 2386 filledCells: 6, 2387 }, 2388 }, 2389 { 2390 name: "All columns past threshold", 2391 cols: 4, 2392 recent: 4, 2393 rows: []*statepb.Row{ 2394 { 2395 Name: "four passes", 2396 Results: []int32{ 2397 int32(statuspb.TestStatus_PASS), 4, 2398 }, 2399 }, 2400 { 2401 Name: "four fails", 2402 Results: []int32{ 2403 int32(statuspb.TestStatus_FAIL), 4, 2404 }, 2405 }, 2406 }, 2407 expectedMetrics: gridStats{ 2408 passingCols: 0, 2409 completedCols: 4, 2410 passingCells: 4, 2411 filledCells: 8, 2412 }, 2413 brokenThreshold: .4, 2414 expectedBroken: true, 2415 }, 2416 { 2417 name: "All columns under threshold", 2418 cols: 4, 2419 recent: 4, 2420 rows: []*statepb.Row{ 2421 { 2422 Name: "four passes", 2423 Results: []int32{ 2424 int32(statuspb.TestStatus_PASS), 4, 2425 }, 2426 }, 2427 { 2428 Name: "four fails", 2429 Results: []int32{ 2430 int32(statuspb.TestStatus_FAIL), 4, 2431 }, 2432 }, 2433 }, expectedMetrics: gridStats{ 2434 passingCols: 0, 2435 completedCols: 4, 2436 passingCells: 4, 2437 filledCells: 8, 2438 }, 2439 brokenThreshold: .6, 2440 expectedBroken: false, 2441 }, 2442 { 2443 name: "One column past threshold", 2444 cols: 4, 2445 recent: 4, 2446 rows: []*statepb.Row{ 2447 { 2448 Name: "four passes", 2449 Results: []int32{ 2450 int32(statuspb.TestStatus_PASS), 4, 2451 }, 2452 }, 2453 { 2454 Name: "one pass three fails", 2455 Results: []int32{ 2456 int32(statuspb.TestStatus_FAIL), 1, 2457 int32(statuspb.TestStatus_PASS), 3, 2458 }, 2459 }, 2460 }, 2461 expectedMetrics: gridStats{ 2462 passingCols: 3, 2463 completedCols: 4, 2464 passingCells: 7, 2465 filledCells: 8, 2466 }, 2467 brokenThreshold: .4, 2468 expectedBroken: true, 2469 }, 2470 { 2471 name: "One column under threshold", 2472 cols: 4, 2473 recent: 4, 2474 rows: []*statepb.Row{ 2475 { 2476 Name: "four passes", 2477 Results: []int32{ 2478 int32(statuspb.TestStatus_PASS), 4, 2479 }, 2480 }, 2481 { 2482 Name: "one pass three fails", 2483 Results: []int32{ 2484 int32(statuspb.TestStatus_FAIL), 1, 2485 int32(statuspb.TestStatus_PASS), 3, 2486 }, 2487 }, 2488 }, 2489 expectedMetrics: gridStats{ 2490 passingCols: 3, 2491 completedCols: 4, 2492 passingCells: 7, 2493 filledCells: 8, 2494 }, 2495 brokenThreshold: .6, 2496 expectedBroken: false, 2497 }, 2498 { 2499 name: "many non-passing/non-failing statuses is not broken", 2500 cols: 4, 2501 recent: 4, 2502 rows: []*statepb.Row{ 2503 { 2504 Name: "four aborts (foo)", 2505 Results: []int32{ 2506 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4, 2507 }, 2508 }, 2509 { 2510 Name: "four aborts (bar)", 2511 Results: []int32{ 2512 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4, 2513 }, 2514 }, 2515 { 2516 Name: "four aborts (baz)", 2517 Results: []int32{ 2518 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4, 2519 }, 2520 }, 2521 }, 2522 expectedMetrics: gridStats{ 2523 passingCols: 0, 2524 completedCols: 4, 2525 passingCells: 0, 2526 filledCells: 12, 2527 }, 2528 brokenThreshold: .6, 2529 expectedBroken: false, 2530 }, 2531 { 2532 name: "many non-passing/non-failing statuses + failing statuses, not broken", 2533 cols: 4, 2534 recent: 4, 2535 rows: []*statepb.Row{ 2536 { 2537 Name: "four aborts (foo)", 2538 Results: []int32{ 2539 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4, 2540 }, 2541 }, 2542 { 2543 Name: "four aborts (bar)", 2544 Results: []int32{ 2545 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4, 2546 }, 2547 }, 2548 { 2549 Name: "four fails", 2550 Results: []int32{ 2551 int32(statuspb.TestStatus_FAIL), 4, 2552 }, 2553 }, 2554 }, 2555 expectedMetrics: gridStats{ 2556 passingCols: 0, 2557 completedCols: 4, 2558 passingCells: 0, 2559 filledCells: 12, 2560 }, 2561 brokenThreshold: .6, 2562 expectedBroken: false, 2563 }, 2564 { 2565 name: "allow ignored but no ignored test statuses", 2566 cols: 4, 2567 recent: 4, 2568 rows: []*statepb.Row{ 2569 { 2570 Name: "four aborts (foo)", 2571 Results: []int32{ 2572 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4, 2573 }, 2574 }, 2575 { 2576 Name: "four aborts (bar)", 2577 Results: []int32{ 2578 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4, 2579 }, 2580 }, 2581 }, 2582 features: FeatureFlags{ 2583 AllowIgnoredColumns: true, 2584 }, 2585 expectedMetrics: gridStats{ 2586 passingCols: 0, 2587 completedCols: 4, 2588 passingCells: 0, 2589 filledCells: 8, 2590 ignoredCols: 0, 2591 }, 2592 }, 2593 { 2594 name: "allow ignored with ignored test statuses", 2595 cols: 4, 2596 recent: 4, 2597 rows: []*statepb.Row{ 2598 { 2599 Name: "abort with passes", 2600 Results: []int32{ 2601 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1, 2602 int32(statuspb.TestStatus_PASS), 3, 2603 }, 2604 }, 2605 { 2606 Name: "unknown with fails", 2607 Results: []int32{ 2608 int32(statuspb.TestStatus_FAIL), 1, 2609 int32(statuspb.TestStatus_UNKNOWN), 1, 2610 int32(statuspb.TestStatus_FAIL), 2, 2611 }, 2612 }, 2613 }, 2614 opts: &configpb.DashboardTabStatusCustomizationOptions{ 2615 IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{ 2616 configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT, 2617 configpb.DashboardTabStatusCustomizationOptions_UNKNOWN, 2618 }, 2619 }, 2620 features: FeatureFlags{ 2621 AllowIgnoredColumns: true, 2622 }, 2623 expectedMetrics: gridStats{ 2624 passingCols: 0, 2625 completedCols: 4, 2626 passingCells: 3, 2627 filledCells: 8, 2628 ignoredCols: 2, 2629 }, 2630 }, 2631 { 2632 name: "do not allow ignored with ignored test statuses", 2633 cols: 4, 2634 recent: 4, 2635 rows: []*statepb.Row{ 2636 { 2637 Name: "abort with passes", 2638 Results: []int32{ 2639 int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1, 2640 int32(statuspb.TestStatus_PASS), 3, 2641 }, 2642 }, 2643 { 2644 Name: "unknown with fails", 2645 Results: []int32{ 2646 int32(statuspb.TestStatus_FAIL), 1, 2647 int32(statuspb.TestStatus_UNKNOWN), 1, 2648 int32(statuspb.TestStatus_PASS), 2, 2649 }, 2650 }, 2651 }, 2652 opts: &configpb.DashboardTabStatusCustomizationOptions{ 2653 IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{ 2654 configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT, 2655 configpb.DashboardTabStatusCustomizationOptions_UNKNOWN, 2656 }, 2657 }, 2658 features: FeatureFlags{ 2659 AllowIgnoredColumns: false, 2660 }, 2661 expectedMetrics: gridStats{ 2662 passingCols: 3, 2663 completedCols: 4, 2664 passingCells: 5, 2665 filledCells: 8, 2666 ignoredCols: 0, 2667 }, 2668 }, 2669 } 2670 2671 for _, tc := range cases { 2672 t.Run(tc.name, func(t *testing.T) { 2673 if actualMetrics, actualBroken := gridMetrics(tc.cols, tc.rows, tc.recent, tc.brokenThreshold, tc.features, tc.opts); actualMetrics != tc.expectedMetrics || actualBroken != tc.expectedBroken { 2674 t.Errorf("%v: gridMetrics() = %v, %v, want %v, %v", tc.name, actualMetrics, actualBroken, tc.expectedMetrics, tc.expectedBroken) 2675 } 2676 }) 2677 } 2678 } 2679 2680 func TestStatusMessage(t *testing.T) { 2681 cases := []struct { 2682 name string 2683 colCells gridStats 2684 status summarypb.DashboardTabSummary_TabStatus 2685 opts *configpb.DashboardTabStatusCustomizationOptions 2686 want string 2687 }{ 2688 { 2689 name: "no filledCells", 2690 want: noRuns, 2691 }, 2692 { 2693 name: "green path", 2694 colCells: gridStats{ 2695 passingCols: 2, 2696 completedCols: 2, 2697 passingCells: 4, 2698 filledCells: 4, 2699 }, 2700 want: "Tab stats: 2 of 2 (100.0%) recent columns passed (4 of 4 or 100.0% cells)", 2701 }, 2702 { 2703 name: "all red path", 2704 colCells: gridStats{ 2705 passingCols: 0, 2706 completedCols: 2, 2707 passingCells: 0, 2708 filledCells: 4, 2709 }, 2710 want: "Tab stats: 0 of 2 (0.0%) recent columns passed (0 of 4 or 0.0% cells)", 2711 }, 2712 { 2713 name: "all values the same", 2714 colCells: gridStats{ 2715 passingCols: 2, 2716 completedCols: 2, 2717 passingCells: 2, 2718 filledCells: 2, 2719 }, 2720 want: "Tab stats: 2 of 2 (100.0%) recent columns passed (2 of 2 or 100.0% cells)", 2721 }, 2722 { 2723 name: "acceptably flaky without ignored columns", 2724 colCells: gridStats{ 2725 passingCols: 3, 2726 completedCols: 4, 2727 passingCells: 6, 2728 filledCells: 8, 2729 }, 2730 status: summarypb.DashboardTabSummary_ACCEPTABLE, 2731 opts: &configpb.DashboardTabStatusCustomizationOptions{ 2732 MaxAcceptableFlakiness: 50, 2733 }, 2734 want: "Tab stats: 3 of 4 (75.0%) recent columns passed (6 of 8 or 75.0% cells)\nStatus info: Recent flakiness (25.0%) over valid columns is within configured acceptable level of 50.0%.", 2735 }, 2736 { 2737 name: "acceptably flaky with ignored columns", 2738 colCells: gridStats{ 2739 passingCols: 2, 2740 completedCols: 4, 2741 ignoredCols: 1, 2742 passingCells: 4, 2743 filledCells: 8, 2744 }, 2745 status: summarypb.DashboardTabSummary_ACCEPTABLE, 2746 opts: &configpb.DashboardTabStatusCustomizationOptions{ 2747 MaxAcceptableFlakiness: 50, 2748 }, 2749 want: "Tab stats: 2 of 4 (50.0%) recent columns passed (4 of 8 or 50.0% cells). 1 columns ignored\nStatus info: Recent flakiness (33.3%) over valid columns is within configured acceptable level of 50.0%.", 2750 }, 2751 { 2752 name: "pending tab status without ignored columns", 2753 colCells: gridStats{ 2754 passingCols: 2, 2755 completedCols: 3, 2756 passingCells: 4, 2757 filledCells: 6, 2758 }, 2759 status: summarypb.DashboardTabSummary_PENDING, 2760 opts: &configpb.DashboardTabStatusCustomizationOptions{ 2761 MinAcceptableRuns: 4, 2762 }, 2763 want: "Tab stats: 2 of 3 (66.7%) recent columns passed (4 of 6 or 66.7% cells)\nStatus info: Not enough runs", 2764 }, 2765 { 2766 name: "pending tab status with ignored columns", 2767 colCells: gridStats{ 2768 passingCols: 2, 2769 completedCols: 3, 2770 ignoredCols: 1, 2771 passingCells: 4, 2772 filledCells: 6, 2773 }, 2774 status: summarypb.DashboardTabSummary_PENDING, 2775 opts: &configpb.DashboardTabStatusCustomizationOptions{ 2776 MinAcceptableRuns: 4, 2777 }, 2778 want: "Tab stats: 2 of 3 (66.7%) recent columns passed (4 of 6 or 66.7% cells). 1 columns ignored\nStatus info: Not enough runs", 2779 }, 2780 } 2781 2782 for _, tc := range cases { 2783 t.Run(tc.name, func(t *testing.T) { 2784 if actual := statusMessage(tc.colCells, tc.status, tc.opts); actual != tc.want { 2785 t.Errorf("%v: statusMessage() = %q, want %q", tc.name, actual, tc.want) 2786 } 2787 }) 2788 } 2789 } 2790 2791 func TestLatestGreen(t *testing.T) { 2792 cases := []struct { 2793 name string 2794 rows []*statepb.Row 2795 cols []*statepb.Column 2796 expected string 2797 first bool 2798 }{ 2799 { 2800 name: "no recent greens by default", 2801 expected: noGreens, 2802 }, 2803 { 2804 name: "no recent greens by default, first green", 2805 first: true, 2806 expected: noGreens, 2807 }, 2808 { 2809 name: "use build id by default", 2810 rows: []*statepb.Row{ 2811 { 2812 Name: "so pass", 2813 Results: []int32{int32(statuspb.TestStatus_PASS), 4}, 2814 }, 2815 }, 2816 cols: []*statepb.Column{ 2817 { 2818 Build: "correct", 2819 Extra: []string{"wrong"}, 2820 }, 2821 }, 2822 expected: "correct", 2823 }, 2824 { 2825 name: "fall back to build id when headers are missing", 2826 rows: []*statepb.Row{ 2827 { 2828 Name: "so pass", 2829 Results: []int32{int32(statuspb.TestStatus_PASS), 4}, 2830 }, 2831 }, 2832 first: true, 2833 cols: []*statepb.Column{ 2834 { 2835 Build: "fallback", 2836 Extra: []string{}, 2837 }, 2838 }, 2839 expected: "fallback", 2840 }, 2841 { 2842 name: "favor first green", 2843 rows: []*statepb.Row{ 2844 { 2845 Name: "so pass", 2846 Results: []int32{int32(statuspb.TestStatus_PASS), 4}, 2847 }, 2848 }, 2849 cols: []*statepb.Column{ 2850 { 2851 Extra: []string{"hello", "there"}, 2852 }, 2853 { 2854 Extra: []string{"bad", "wrong"}, 2855 }, 2856 }, 2857 first: true, 2858 expected: "hello", 2859 }, 2860 { 2861 name: "accept any kind of pass", 2862 rows: []*statepb.Row{ 2863 { 2864 Name: "pass w/ errors", 2865 Results: []int32{ 2866 int32(statuspb.TestStatus_PASS_WITH_ERRORS), 1, 2867 int32(statuspb.TestStatus_PASS), 1, 2868 }, 2869 }, 2870 { 2871 Name: "pass pass", 2872 Results: []int32{int32(statuspb.TestStatus_PASS), 2}, 2873 }, 2874 { 2875 Name: "pass and skip", 2876 Results: []int32{ 2877 int32(statuspb.TestStatus_PASS_WITH_SKIPS), 1, 2878 int32(statuspb.TestStatus_PASS), 1, 2879 }, 2880 }, 2881 }, 2882 cols: []*statepb.Column{ 2883 { 2884 Extra: []string{"good"}, 2885 }, 2886 { 2887 Extra: []string{"bad"}, 2888 }, 2889 }, 2890 first: true, 2891 expected: "good", 2892 }, 2893 { 2894 name: "avoid columns with running rows", 2895 rows: []*statepb.Row{ 2896 { 2897 Name: "running", 2898 Results: []int32{ 2899 int32(statuspb.TestStatus_RUNNING), 1, 2900 int32(statuspb.TestStatus_PASS), 1, 2901 }, 2902 }, 2903 { 2904 Name: "pass", 2905 Results: []int32{ 2906 int32(statuspb.TestStatus_PASS), 2, 2907 }, 2908 }, 2909 }, 2910 cols: []*statepb.Column{ 2911 { 2912 Extra: []string{"skip-first-col-still-running"}, 2913 }, 2914 { 2915 Extra: []string{"accept second-all-finished"}, 2916 }, 2917 }, 2918 first: true, 2919 expected: "accept second-all-finished", 2920 }, 2921 { 2922 name: "avoid columns with flakes", 2923 rows: []*statepb.Row{ 2924 { 2925 Name: "flaking", 2926 Results: []int32{ 2927 int32(statuspb.TestStatus_FLAKY), 1, 2928 int32(statuspb.TestStatus_PASS), 1, 2929 }, 2930 }, 2931 { 2932 Name: "passing", 2933 Results: []int32{ 2934 int32(statuspb.TestStatus_PASS), 2, 2935 }, 2936 }, 2937 }, 2938 cols: []*statepb.Column{ 2939 { 2940 Extra: []string{"skip-first-col-with-flake"}, 2941 }, 2942 { 2943 Extra: []string{"accept second-no-flake"}, 2944 }, 2945 }, 2946 first: true, 2947 expected: "accept second-no-flake", 2948 }, 2949 { 2950 name: "avoid columns with failures", 2951 rows: []*statepb.Row{ 2952 { 2953 Name: "failing", 2954 Results: []int32{ 2955 int32(statuspb.TestStatus_FAIL), 1, 2956 int32(statuspb.TestStatus_PASS), 1, 2957 }, 2958 }, 2959 { 2960 Name: "passing", 2961 Results: []int32{ 2962 int32(statuspb.TestStatus_PASS), 2, 2963 }, 2964 }, 2965 }, 2966 cols: []*statepb.Column{ 2967 { 2968 Extra: []string{"skip-first-col-with-fail"}, 2969 }, 2970 { 2971 Extra: []string{"accept second-after-failure"}, 2972 }, 2973 }, 2974 first: true, 2975 expected: "accept second-after-failure", 2976 }, 2977 { 2978 name: "multiple failing columns fixed", 2979 rows: []*statepb.Row{ 2980 { 2981 Name: "fail then pass", 2982 Results: []int32{ 2983 int32(statuspb.TestStatus_FAIL), 1, 2984 int32(statuspb.TestStatus_PASS), 1, 2985 }, 2986 }, 2987 { 2988 Name: "also fail then pass", 2989 Results: []int32{ 2990 int32(statuspb.TestStatus_FAIL), 1, 2991 int32(statuspb.TestStatus_PASS), 2, 2992 }, 2993 }, 2994 }, 2995 cols: []*statepb.Column{ 2996 { 2997 Extra: []string{"skip-first-col-with-fail"}, 2998 }, 2999 { 3000 Extra: []string{"accept second-after-failure"}, 3001 }, 3002 }, 3003 first: true, 3004 expected: "accept second-after-failure", 3005 }, 3006 } 3007 3008 for _, tc := range cases { 3009 t.Run(tc.name, func(t *testing.T) { 3010 grid := statepb.Grid{ 3011 Columns: tc.cols, 3012 Rows: tc.rows, 3013 } 3014 if actual := latestGreen(&grid, tc.first); actual != tc.expected { 3015 t.Errorf("%s != expected %s", actual, tc.expected) 3016 } 3017 }) 3018 } 3019 } 3020 3021 func TestGetHealthinessForInterval(t *testing.T) { 3022 now := int64(1000000) // arbitrary time 3023 secondsInDay := int64(86400) 3024 // These values are *1000 because Column.Started is in milliseconds 3025 withinCurrentInterval := (float64(now) - 0.5*float64(secondsInDay)) * 1000.0 3026 withinPreviousInterval := (float64(now) - 1.5*float64(secondsInDay)) * 1000.0 3027 notWithinAnyInterval := (float64(now) - 3.0*float64(secondsInDay)) * 1000.0 3028 cases := []struct { 3029 name string 3030 grid *statepb.Grid 3031 tabName string 3032 interval int 3033 expected *summarypb.HealthinessInfo 3034 }{ 3035 { 3036 name: "typical inputs returns correct HealthinessInfo", 3037 grid: &statepb.Grid{ 3038 Columns: []*statepb.Column{ 3039 {Started: withinCurrentInterval}, 3040 {Started: withinCurrentInterval}, 3041 {Started: withinPreviousInterval}, 3042 {Started: withinPreviousInterval}, 3043 {Started: notWithinAnyInterval}, 3044 }, 3045 Rows: []*statepb.Row{ 3046 { 3047 Name: "test_1", 3048 Results: []int32{ 3049 statuspb.TestStatus_value["PASS"], 1, 3050 statuspb.TestStatus_value["FAIL"], 1, 3051 statuspb.TestStatus_value["FAIL"], 1, 3052 statuspb.TestStatus_value["FAIL"], 2, 3053 }, 3054 Messages: []string{ 3055 "", 3056 "", 3057 "", 3058 "infra_fail_1", 3059 "", 3060 }, 3061 }, 3062 }, 3063 }, 3064 tabName: "tab1", 3065 interval: 1, // enforce that this equals what secondsInDay is multiplied by below in the Timestamps 3066 expected: &summarypb.HealthinessInfo{ 3067 Start: ×tamp.Timestamp{Seconds: now - secondsInDay}, 3068 End: ×tamp.Timestamp{Seconds: now}, 3069 Tests: []*summarypb.TestInfo{ 3070 { 3071 DisplayName: "test_1", 3072 TotalNonInfraRuns: 2, 3073 PassedNonInfraRuns: 1, 3074 FailedNonInfraRuns: 1, 3075 TotalRunsWithInfra: 2, 3076 Flakiness: 50.0, 3077 PreviousFlakiness: []float32{100.0}, 3078 ChangeFromLastInterval: summarypb.TestInfo_DOWN, 3079 }, 3080 }, 3081 AverageFlakiness: 50.0, 3082 PreviousFlakiness: []float32{100.0}, 3083 }, 3084 }, 3085 } 3086 3087 for _, tc := range cases { 3088 t.Run(tc.name, func(t *testing.T) { 3089 if actual := getHealthinessForInterval(tc.grid, tc.tabName, time.Unix(now, 0), tc.interval); !proto.Equal(actual, tc.expected) { 3090 t.Errorf("actual: %+v != expected: %+v", actual, tc.expected) 3091 } 3092 }) 3093 } 3094 } 3095 3096 func TestGoBackDays(t *testing.T) { 3097 cases := []struct { 3098 name string 3099 days int 3100 currentTime time.Time 3101 expected int 3102 }{ 3103 { 3104 name: "0 days returns same Time as input", 3105 days: 0, 3106 currentTime: time.Unix(0, 0).UTC(), 3107 expected: 0, 3108 }, 3109 { 3110 name: "positive days input returns that many days in the past", 3111 days: 7, 3112 currentTime: time.Unix(0, 0).UTC().AddDate(0, 0, 7), // Gives a date 7 days after Unix 0 time 3113 expected: 0, 3114 }, 3115 } 3116 3117 for _, tc := range cases { 3118 t.Run(tc.name, func(t *testing.T) { 3119 if actual := goBackDays(tc.days, tc.currentTime); actual != tc.expected { 3120 t.Errorf("goBackDays gave actual: %d != expected: %d for days: %d and currentTime: %+v", actual, tc.expected, tc.days, tc.currentTime) 3121 } 3122 }) 3123 } 3124 } 3125 3126 func TestShouldRunHealthiness(t *testing.T) { 3127 cases := []struct { 3128 name string 3129 tab *configpb.DashboardTab 3130 expected bool 3131 }{ 3132 { 3133 name: "tab with false Enable returns false", 3134 tab: &configpb.DashboardTab{ 3135 HealthAnalysisOptions: &configpb.HealthAnalysisOptions{ 3136 Enable: false, 3137 }, 3138 }, 3139 expected: false, 3140 }, 3141 { 3142 name: "tab with true Enable returns true", 3143 tab: &configpb.DashboardTab{ 3144 HealthAnalysisOptions: &configpb.HealthAnalysisOptions{ 3145 Enable: true, 3146 }, 3147 }, 3148 expected: true, 3149 }, 3150 { 3151 name: "tab with nil HealthAnalysisOptions returns false", 3152 tab: &configpb.DashboardTab{}, 3153 expected: false, 3154 }, 3155 } 3156 3157 for _, tc := range cases { 3158 t.Run(tc.name, func(t *testing.T) { 3159 if actual := shouldRunHealthiness(tc.tab); actual != tc.expected { 3160 t.Errorf("actual: %t != expected: %t", actual, tc.expected) 3161 } 3162 }) 3163 } 3164 } 3165 3166 func TestCoalesceResult(t *testing.T) { 3167 cases := []struct { 3168 name string 3169 result statuspb.TestStatus 3170 running bool 3171 expected statuspb.TestStatus 3172 }{ 3173 { 3174 name: "no result by default", 3175 expected: statuspb.TestStatus_NO_RESULT, 3176 }, 3177 { 3178 name: "running is no result when ignored", 3179 result: statuspb.TestStatus_RUNNING, 3180 expected: statuspb.TestStatus_NO_RESULT, 3181 running: result.IgnoreRunning, 3182 }, 3183 { 3184 name: "running is neutral when shown", 3185 result: statuspb.TestStatus_RUNNING, 3186 expected: statuspb.TestStatus_UNKNOWN, 3187 running: result.ShowRunning, 3188 }, 3189 { 3190 name: "fail is fail", 3191 result: statuspb.TestStatus_FAIL, 3192 expected: statuspb.TestStatus_FAIL, 3193 }, 3194 { 3195 name: "flaky is flaky", 3196 result: statuspb.TestStatus_FLAKY, 3197 expected: statuspb.TestStatus_FLAKY, 3198 }, 3199 { 3200 name: "simplify pass", 3201 result: statuspb.TestStatus_PASS_WITH_ERRORS, 3202 expected: statuspb.TestStatus_PASS, 3203 }, 3204 { 3205 name: "categorized abort is neutral", 3206 result: statuspb.TestStatus_CATEGORIZED_ABORT, 3207 expected: statuspb.TestStatus_UNKNOWN, 3208 }, 3209 } 3210 3211 for _, tc := range cases { 3212 t.Run(tc.name, func(t *testing.T) { 3213 if actual := coalesceResult(tc.result, tc.running); actual != tc.expected { 3214 t.Errorf("actual %s != expected %s", actual, tc.expected) 3215 } 3216 }) 3217 } 3218 } 3219 3220 func TestResultIter(t *testing.T) { 3221 cases := []struct { 3222 name string 3223 cancel int 3224 in []int32 3225 expected []statuspb.TestStatus 3226 }{ 3227 { 3228 name: "basically works", 3229 in: []int32{ 3230 int32(statuspb.TestStatus_PASS), 3, 3231 int32(statuspb.TestStatus_FAIL), 2, 3232 }, 3233 expected: []statuspb.TestStatus{ 3234 statuspb.TestStatus_PASS, 3235 statuspb.TestStatus_PASS, 3236 statuspb.TestStatus_PASS, 3237 statuspb.TestStatus_FAIL, 3238 statuspb.TestStatus_FAIL, 3239 }, 3240 }, 3241 { 3242 name: "ignore last unbalanced input", 3243 in: []int32{ 3244 int32(statuspb.TestStatus_PASS), 3, 3245 int32(statuspb.TestStatus_FAIL), 3246 }, 3247 expected: []statuspb.TestStatus{ 3248 statuspb.TestStatus_PASS, 3249 statuspb.TestStatus_PASS, 3250 statuspb.TestStatus_PASS, 3251 }, 3252 }, 3253 } 3254 3255 for _, tc := range cases { 3256 t.Run(tc.name, func(t *testing.T) { 3257 iter := result.Iter(tc.in) 3258 var actual []statuspb.TestStatus 3259 var idx int 3260 for { 3261 val, ok := iter() 3262 if !ok { 3263 return 3264 } 3265 idx++ 3266 actual = append(actual, val) 3267 } 3268 if !cmp.Equal(actual, tc.expected, protocmp.Transform()) { 3269 t.Errorf("%s != expected %s", actual, tc.expected) 3270 } 3271 }) 3272 } 3273 } 3274 3275 func TestSummaryPath(t *testing.T) { 3276 mustPath := func(s string) *gcs.Path { 3277 p, err := gcs.NewPath(s) 3278 if err != nil { 3279 t.Fatalf("gcs.NewPath(%q) got err: %v", s, err) 3280 } 3281 return p 3282 } 3283 cases := []struct { 3284 name string 3285 path gcs.Path 3286 prefix string 3287 dash string 3288 want *gcs.Path 3289 err bool 3290 }{ 3291 { 3292 name: "normal", 3293 path: *mustPath("gs://bucket/config"), 3294 dash: "hello", 3295 want: mustPath("gs://bucket/summary-hello"), 3296 }, 3297 { 3298 name: "prefix", // construct path with a prefix correctly 3299 path: *mustPath("gs://bucket/config"), 3300 prefix: "summary", 3301 dash: "hello", 3302 want: mustPath("gs://bucket/summary/summary-hello"), 3303 }, 3304 { 3305 name: "normalize", // normalize dashboard name correctly 3306 path: *mustPath("gs://bucket/config"), 3307 prefix: "UpperCase", // do not normalize 3308 dash: "Hello --- World", // normalize 3309 want: mustPath("gs://bucket/UpperCase/summary-helloworld"), 3310 }, 3311 } 3312 3313 for _, tc := range cases { 3314 t.Run(tc.name, func(t *testing.T) { 3315 got, err := SummaryPath(tc.path, tc.prefix, tc.dash) 3316 switch { 3317 case err != nil: 3318 if !tc.err { 3319 t.Errorf("summaryPath(%q, %q, %q) got unexpected error: %v", tc.path, tc.prefix, tc.dash, err) 3320 } 3321 case tc.err: 3322 t.Errorf("summaryPath(%q, %q, %q) failed to get an error", tc.path, tc.prefix, tc.name) 3323 default: 3324 if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(gcs.Path{})); diff != "" { 3325 t.Errorf("summaryPath(%q, %q, %q) got unexpected diff (-want +got):\n%s", tc.path, tc.prefix, tc.dash, diff) 3326 } 3327 } 3328 }) 3329 } 3330 } 3331 3332 func TestTestResultLink(t *testing.T) { 3333 cases := []struct { 3334 name string 3335 template *configpb.LinkTemplate 3336 properties map[string]string 3337 testID string 3338 target string 3339 buildID string 3340 gcsPrefix string 3341 propertyToColumnHeader map[string]string 3342 customColumnHeaders map[string]string 3343 want string 3344 }{ 3345 { 3346 name: "nil", 3347 want: "", 3348 }, 3349 { 3350 name: "empty", 3351 template: &configpb.LinkTemplate{ 3352 Url: "https://test.com/<encode:<workflow-name>>/<workflow-id>/<test-id>/<encode:<test-name>>", 3353 Options: []*configpb.LinkOptionsTemplate{ 3354 { 3355 Key: "prefix", 3356 Value: "<gcs-prefix>", 3357 }, 3358 { 3359 Key: "build", 3360 Value: "<build-id>", 3361 }, 3362 { 3363 Key: "prop", 3364 Value: "<my-prop>", 3365 }, 3366 { 3367 Key: "foo", 3368 Value: "<custom-0>", 3369 }, 3370 }, 3371 }, 3372 properties: map[string]string{}, 3373 testID: "", 3374 target: "", 3375 buildID: "", 3376 gcsPrefix: "", 3377 propertyToColumnHeader: map[string]string{}, 3378 customColumnHeaders: map[string]string{}, 3379 want: "https://test.com/%3Cencode:%3Cworkflow-name%3E%3E/%3Cworkflow-id%3E//?build=&foo=%3Ccustom-0%3E&prefix=&prop=%3Cmy-prop%3E", 3380 }, 3381 { 3382 name: "empty template", 3383 template: &configpb.LinkTemplate{}, 3384 properties: map[string]string{ 3385 "workflow-id": "workflow-id-1", 3386 "workflow-name": "//my:workflow", 3387 "my-prop": "foo", 3388 }, 3389 testID: "my-test-id-1", 3390 target: "//path/to:my-test", 3391 buildID: "build-1", 3392 gcsPrefix: "my-bucket/has/results", 3393 propertyToColumnHeader: map[string]string{ 3394 "<custom-0>": "apple", 3395 }, 3396 customColumnHeaders: map[string]string{ 3397 "apple": "fruit", 3398 }, 3399 want: "", 3400 }, 3401 { 3402 name: "basically works", 3403 template: &configpb.LinkTemplate{ 3404 Url: "https://test.com/<encode:<workflow-name>>/<workflow-id>/<test-id>/<encode:<test-name>>", 3405 Options: []*configpb.LinkOptionsTemplate{ 3406 { 3407 Key: "prefix", 3408 Value: "<gcs-prefix>", 3409 }, 3410 { 3411 Key: "build", 3412 Value: "<build-id>", 3413 }, 3414 { 3415 Key: "prop", 3416 Value: "<my-prop>", 3417 }, 3418 { 3419 Key: "foo", 3420 Value: "<custom-0>", 3421 }, 3422 { 3423 Key: "hello", 3424 Value: "<custom-1>", 3425 }, 3426 }, 3427 }, 3428 properties: map[string]string{ 3429 "workflow-id": "workflow-id-1", 3430 "workflow-name": "//my:workflow", 3431 "my-prop": "foo", 3432 }, 3433 testID: "my-test-id-1", 3434 target: "//path/to:my-test", 3435 buildID: "build-1", 3436 gcsPrefix: "my-bucket/has/results", 3437 propertyToColumnHeader: map[string]string{ 3438 "<custom-0>": "foo", 3439 "<custom-1>": "hello", 3440 }, 3441 customColumnHeaders: map[string]string{ 3442 "foo": "bar", 3443 "hello": "world", 3444 }, 3445 want: "https://test.com/%2F%2Fmy:workflow/workflow-id-1/my-test-id-1/%2F%2Fpath%2Fto:my-test?build=build-1&foo=bar&hello=world&prefix=my-bucket%2Fhas%2Fresults&prop=foo", 3446 }, 3447 { 3448 name: "non-matching tokens", 3449 template: &configpb.LinkTemplate{ 3450 Url: "https://test.com/<greeting>", 3451 Options: []*configpb.LinkOptionsTemplate{ 3452 { 3453 Key: "farewell", 3454 Value: "<farewell>", 3455 }, 3456 { 3457 Key: "bye", 3458 Value: "<custom-0>", 3459 }, 3460 }, 3461 }, 3462 properties: map[string]string{ 3463 "workflow-id": "workflow-id-1", 3464 "workflow-name": "//my:workflow", 3465 "my-prop": "foo", 3466 }, 3467 propertyToColumnHeader: map[string]string{ 3468 "<custom-0>": "bye", 3469 }, 3470 customColumnHeaders: map[string]string{ 3471 "foo": "bar", 3472 }, 3473 testID: "my-test-id-1", 3474 target: "//path/to:my-test", 3475 buildID: "build-1", 3476 gcsPrefix: "my-bucket/has/results", 3477 want: "https://test.com/%3Cgreeting%3E?bye=%3Ccustom-0%3E&farewell=%3Cfarewell%3E", 3478 }, 3479 { 3480 name: "basically works, nil properties", 3481 template: &configpb.LinkTemplate{ 3482 Url: "https://test.com/<encode:<workflow-name>>/<workflow-id>/<test-id>/<encode:<test-name>>", 3483 Options: []*configpb.LinkOptionsTemplate{ 3484 { 3485 Key: "prefix", 3486 Value: "<gcs-prefix>", 3487 }, 3488 { 3489 Key: "build", 3490 Value: "<build-id>", 3491 }, 3492 }, 3493 }, 3494 properties: nil, 3495 testID: "my-test-id-1", 3496 target: "//path/to:my-test", 3497 buildID: "build-1", 3498 gcsPrefix: "my-bucket/has/results", 3499 want: "https://test.com/%3Cencode:%3Cworkflow-name%3E%3E/%3Cworkflow-id%3E/my-test-id-1///path/to:my-test?build=build-1&prefix=my-bucket%2Fhas%2Fresults", 3500 }, 3501 } 3502 3503 for _, tc := range cases { 3504 t.Run(tc.name, func(t *testing.T) { 3505 if got := testResultLink(tc.template, tc.properties, tc.testID, tc.target, tc.buildID, tc.gcsPrefix, tc.propertyToColumnHeader, tc.customColumnHeaders); got != tc.want { 3506 t.Errorf("testResultLink(%v, %v, %s, %s, %s, %s, %s, %s) = %q, want %q", tc.template, tc.properties, tc.testID, tc.target, tc.buildID, tc.gcsPrefix, tc.propertyToColumnHeader, tc.customColumnHeaders, got, tc.want) 3507 } 3508 }) 3509 } 3510 }