github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/updater_test.go (about) 1 /* 2 Copyright 2018 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 updater 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "net/http" 25 "reflect" 26 "sort" 27 "sync" 28 "testing" 29 "time" 30 31 "cloud.google.com/go/storage" 32 "github.com/GoogleCloudPlatform/testgrid/config" 33 "github.com/GoogleCloudPlatform/testgrid/metadata" 34 _ "github.com/GoogleCloudPlatform/testgrid/metadata/junit" 35 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 36 statepb "github.com/GoogleCloudPlatform/testgrid/pb/state" 37 statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status" 38 "github.com/GoogleCloudPlatform/testgrid/util/gcs" 39 "github.com/GoogleCloudPlatform/testgrid/util/gcs/fake" 40 "github.com/fvbommel/sortorder" 41 "github.com/golang/protobuf/ptypes/timestamp" 42 "github.com/google/go-cmp/cmp" 43 "github.com/sirupsen/logrus" 44 "google.golang.org/api/googleapi" 45 "google.golang.org/protobuf/testing/protocmp" 46 core "k8s.io/api/core/v1" 47 ) 48 49 type fakeUpload = fake.Upload 50 type fakeStater = fake.Stater 51 type fakeStat = fake.Stat 52 type fakeUploader = fake.Uploader 53 type fakeUploadClient = fake.UploadClient 54 type fakeLister = fake.Lister 55 type fakeOpener = fake.Opener 56 57 func TestGCS(t *testing.T) { 58 cases := []struct { 59 name string 60 ctx context.Context 61 group *configpb.TestGroup 62 fail bool 63 }{ 64 { 65 name: "contextless", 66 group: &configpb.TestGroup{}, 67 fail: true, 68 }, 69 { 70 name: "basic", 71 ctx: context.Background(), 72 group: &configpb.TestGroup{}, 73 }, 74 { 75 name: "kubernetes", // should fail 76 ctx: context.Background(), 77 group: &configpb.TestGroup{ 78 UseKubernetesClient: true, 79 }, 80 fail: true, 81 }, 82 } 83 84 for _, tc := range cases { 85 t.Run(tc.name, func(t *testing.T) { 86 // Goal here is to ignore for non-k8s client otherwise if we get past this check 87 // send updater() arguments that should fail if it tries to do anything, 88 // either because the context is canceled or things like client are unset) 89 ctx, cancel := context.WithCancel(context.Background()) 90 cancel() 91 defer func() { 92 if r := recover(); r != nil { 93 if !tc.fail { 94 t.Errorf("updater() got an unexpected panic: %#v", r) 95 } 96 } 97 }() 98 updater := GCS(tc.ctx, nil, 0, 0, 0, false, false) 99 _, err := updater(ctx, logrus.WithField("case", tc.name), nil, tc.group, gcs.Path{}) 100 switch { 101 case err != nil: 102 if !tc.fail { 103 t.Errorf("updater() got unexpected error: %v", err) 104 } 105 case tc.fail: 106 t.Error("updater() failed to return an error") 107 } 108 }) 109 } 110 } 111 112 func TestUpdate(t *testing.T) { 113 defaultTimeout := 5 * time.Minute 114 configPath := newPathOrDie("gs://bucket/path/to/config") 115 cases := []struct { 116 name string 117 ctx context.Context 118 config *configpb.Configuration 119 configErr error 120 builds map[string][]fakeBuild 121 gridPrefix string 122 groupConcurrency int 123 buildConcurrency int 124 skipConfirm bool 125 groupUpdater GroupUpdater 126 groupTimeout *time.Duration 127 buildTimeout *time.Duration 128 groupNames []string 129 freq time.Duration 130 131 expected fakeUploader 132 err bool 133 successes int 134 errors int 135 skips int 136 }{ 137 { 138 name: "basically works", 139 config: &configpb.Configuration{ 140 TestGroups: []*configpb.TestGroup{ 141 { 142 Name: "hello", 143 GcsPrefix: "kubernetes-jenkins/path/to/job", 144 DaysOfResults: 7, 145 UseKubernetesClient: true, 146 NumColumnsRecent: 6, 147 }, 148 { 149 Name: "modern", 150 DaysOfResults: 7, 151 NumColumnsRecent: 6, 152 ResultSource: &configpb.TestGroup_ResultSource{ 153 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 154 GcsConfig: &configpb.GCSConfig{ 155 GcsPrefix: "kubernetes-jenkins/path/to/another-job", 156 }, 157 }, 158 }, 159 }, 160 { 161 Name: "skip-non-k8s", 162 GcsPrefix: "kubernetes-jenkins/path/to/job", 163 DaysOfResults: 7, 164 NumColumnsRecent: 6, 165 }, 166 }, 167 Dashboards: []*configpb.Dashboard{ 168 { 169 Name: "dash", 170 DashboardTab: []*configpb.DashboardTab{ 171 { 172 Name: "hello-tab", 173 TestGroupName: "hello", 174 }, 175 { 176 Name: "modern-tab", 177 TestGroupName: "modern", 178 }, 179 { 180 Name: "skip-tab", 181 TestGroupName: "skip-non-k8s", 182 }, 183 }, 184 }, 185 }, 186 }, 187 expected: fakeUploader{ 188 *resolveOrDie(&configPath, "hello"): { 189 Buf: mustGrid(&statepb.Grid{}), 190 CacheControl: "no-cache", 191 WorldRead: gcs.DefaultACL, 192 Generation: 2, 193 }, 194 *resolveOrDie(&configPath, "modern"): { 195 Buf: mustGrid(&statepb.Grid{}), 196 CacheControl: "no-cache", 197 Generation: 2, 198 WorldRead: gcs.DefaultACL, 199 }, 200 *resolveOrDie(&configPath, "skip-non-k8s"): { 201 Buf: mustGrid(&statepb.Grid{}), 202 CacheControl: "no-cache", 203 WorldRead: gcs.DefaultACL, 204 Generation: 1, 205 }, 206 }, 207 successes: 3, 208 }, 209 { 210 name: "bad grid prefix", 211 gridPrefix: "!@#$%^&*()", 212 config: &configpb.Configuration{ 213 TestGroups: []*configpb.TestGroup{ 214 { 215 Name: "hello", 216 GcsPrefix: "kubernetes-jenkins/path/to/job", 217 DaysOfResults: 7, 218 UseKubernetesClient: true, 219 NumColumnsRecent: 6, 220 }, 221 }, 222 Dashboards: []*configpb.Dashboard{ 223 { 224 Name: "dash", 225 DashboardTab: []*configpb.DashboardTab{ 226 { 227 Name: "hello-tab", 228 TestGroupName: "hello", 229 }, 230 }, 231 }, 232 }, 233 }, 234 expected: fakeUploader{}, 235 err: true, 236 }, 237 { 238 name: "update specified", 239 config: &configpb.Configuration{ 240 TestGroups: []*configpb.TestGroup{ 241 { 242 Name: "hello", 243 GcsPrefix: "kubernetes-jenkins/path/to/job", 244 DaysOfResults: 7, 245 UseKubernetesClient: true, 246 NumColumnsRecent: 6, 247 }, 248 { 249 Name: "hiya", 250 GcsPrefix: "kubernetes-jenkins/path/to/job", 251 DaysOfResults: 7, 252 UseKubernetesClient: true, 253 NumColumnsRecent: 6, 254 }, 255 { 256 Name: "goodbye", 257 GcsPrefix: "kubernetes-jenkins/path/to/job", 258 DaysOfResults: 7, 259 UseKubernetesClient: true, 260 NumColumnsRecent: 6, 261 }, 262 }, 263 Dashboards: []*configpb.Dashboard{ 264 { 265 Name: "dash", 266 DashboardTab: []*configpb.DashboardTab{ 267 { 268 Name: "hello-tab", 269 TestGroupName: "hello", 270 }, 271 { 272 Name: "hiya-tab", 273 TestGroupName: "hiya", 274 }, 275 { 276 Name: "goodbye-tab", 277 TestGroupName: "goodbye", 278 }, 279 }, 280 }, 281 }, 282 }, 283 groupNames: []string{"hello", "hiya"}, 284 expected: fakeUploader{ 285 *resolveOrDie(&configPath, "hello"): { 286 Buf: mustGrid(&statepb.Grid{}), 287 CacheControl: "no-cache", 288 WorldRead: gcs.DefaultACL, 289 Generation: 2, 290 }, 291 *resolveOrDie(&configPath, "hiya"): { 292 Buf: mustGrid(&statepb.Grid{}), 293 CacheControl: "no-cache", 294 Generation: 2, 295 WorldRead: gcs.DefaultACL, 296 }, 297 }, 298 successes: 2, 299 }, 300 { 301 name: "update error with freq = 0", 302 config: &configpb.Configuration{ 303 TestGroups: []*configpb.TestGroup{ 304 { 305 Name: "hello", 306 GcsPrefix: "kubernetes-jenkins/path/to/job", 307 DaysOfResults: 7, 308 UseKubernetesClient: true, 309 NumColumnsRecent: 6, 310 }, 311 { 312 Name: "world", 313 GcsPrefix: "kubernetes-jenkins/path/to/job", 314 DaysOfResults: 7, 315 UseKubernetesClient: true, 316 NumColumnsRecent: 6, 317 }, 318 }, 319 Dashboards: []*configpb.Dashboard{ 320 { 321 Name: "dash", 322 DashboardTab: []*configpb.DashboardTab{ 323 { 324 Name: "hello-tab", 325 TestGroupName: "hello", 326 }, 327 { 328 Name: "world-tab", 329 TestGroupName: "world", 330 }, 331 }, 332 }, 333 }, 334 }, 335 groupUpdater: func(_ context.Context, _ logrus.FieldLogger, _ gcs.Client, tg *configpb.TestGroup, _ gcs.Path) (bool, error) { 336 if tg.Name == "world" { 337 return false, &googleapi.Error{ 338 Code: http.StatusPreconditionFailed, 339 } 340 } 341 return false, errors.New("bad update") 342 343 }, 344 builds: make(map[string][]fakeBuild), 345 freq: time.Duration(0), 346 expected: fakeUploader{ 347 *resolveOrDie(&configPath, "hello"): { 348 Buf: mustGrid(&statepb.Grid{}), 349 CacheControl: "no-cache", 350 WorldRead: gcs.DefaultACL, 351 Generation: 1, 352 }, 353 *resolveOrDie(&configPath, "world"): { 354 Buf: mustGrid(&statepb.Grid{}), 355 CacheControl: "no-cache", 356 WorldRead: gcs.DefaultACL, 357 Generation: 1, 358 }, 359 }, 360 errors: 1, 361 skips: 1, 362 }, 363 // TODO(fejta): more cases 364 } 365 366 for _, tc := range cases { 367 t.Run(tc.name, func(t *testing.T) { 368 if tc.ctx == nil { 369 tc.ctx = context.Background() 370 } 371 ctx, cancel := context.WithCancel(tc.ctx) 372 defer cancel() 373 374 if tc.groupConcurrency == 0 { 375 tc.groupConcurrency = 1 376 } 377 if tc.buildConcurrency == 0 { 378 tc.buildConcurrency = 1 379 } 380 if tc.groupTimeout == nil { 381 tc.groupTimeout = &defaultTimeout 382 } 383 if tc.buildTimeout == nil { 384 tc.buildTimeout = &defaultTimeout 385 } 386 387 client := &fake.ConditionalClient{ 388 UploadClient: fake.UploadClient{ 389 Uploader: fakeUploader{}, 390 Client: fakeClient{ 391 Lister: fakeLister{}, 392 Opener: fakeOpener{ 393 Paths: map[gcs.Path]fake.Object{}, 394 }, 395 }, 396 }, 397 Lock: &sync.RWMutex{}, 398 } 399 400 client.Opener.Paths[configPath] = fakeObject{ 401 Data: func() string { 402 b, err := config.MarshalBytes(tc.config) 403 if err != nil { 404 t.Fatalf("config.MarshalBytes() errored: %v", err) 405 } 406 return string(b) 407 }(), 408 Attrs: &storage.ReaderObjectAttrs{}, 409 ReadErr: tc.configErr, 410 } 411 412 for _, group := range tc.config.TestGroups { 413 builds, ok := tc.builds[group.Name] 414 if !ok { 415 continue 416 } 417 buildsPath := newPathOrDie("gs://" + group.GcsPrefix) 418 fi := client.Lister[buildsPath] 419 for _, build := range addBuilds(&client.Client, buildsPath, builds...) { 420 fi.Objects = append(fi.Objects, storage.ObjectAttrs{ 421 Prefix: build.Path.Object(), 422 }) 423 } 424 client.Lister[buildsPath] = fi 425 } 426 427 if tc.groupUpdater == nil { 428 poolCtx, poolCancel := context.WithCancel(context.Background()) 429 defer poolCancel() 430 tc.groupUpdater = GCS(poolCtx, client, *tc.groupTimeout, *tc.buildTimeout, tc.buildConcurrency, !tc.skipConfirm, false) 431 } 432 opts := &UpdateOptions{ 433 ConfigPath: configPath, 434 GridPrefix: tc.gridPrefix, 435 GroupConcurrency: tc.groupConcurrency, 436 GroupNames: tc.groupNames, 437 Write: !tc.skipConfirm, 438 Freq: tc.freq, 439 } 440 err := Update( 441 ctx, 442 client, 443 nil, // metric, 444 tc.groupUpdater, 445 opts, 446 ) 447 switch { 448 case err != nil: 449 if !tc.err { 450 t.Errorf("Update() got unexpected error: %v", err) 451 } 452 case tc.err: 453 t.Error("Update() failed to receive an error") 454 default: 455 actual := client.Uploader 456 if diff := cmp.Diff(tc.expected, actual, cmp.AllowUnexported(fakeUpload{})); diff != "" { 457 t.Errorf("Update() uploaded files got unexpected diff (-want, +got):\n%s", diff) 458 } 459 } 460 }) 461 } 462 } 463 464 func TestTestGroupPath(t *testing.T) { 465 path := newPathOrDie("gs://bucket/config") 466 pNewPathOrDie := func(s string) *gcs.Path { 467 p := newPathOrDie(s) 468 return &p 469 } 470 cases := []struct { 471 name string 472 groupName string 473 gridPrefix string 474 expected *gcs.Path 475 }{ 476 { 477 name: "basically works", 478 expected: &path, 479 }, 480 { 481 name: "invalid group name errors", 482 groupName: "---://foo", 483 }, 484 { 485 name: "bucket change errors", 486 groupName: "gs://honey-bucket/config", 487 }, 488 { 489 name: "normal behavior works", 490 groupName: "random-group", 491 expected: pNewPathOrDie("gs://bucket/random-group"), 492 }, 493 { 494 name: "target a subfolder works", 495 groupName: "beta/random-group", 496 expected: pNewPathOrDie("gs://bucket/beta/random-group"), 497 }, 498 { 499 name: "resolve reference fails", 500 groupName: "http://bucket/config", 501 }, 502 } 503 504 for _, tc := range cases { 505 t.Run(tc.name, func(t *testing.T) { 506 actual, err := TestGroupPath(path, tc.gridPrefix, tc.groupName) 507 switch { 508 case err != nil: 509 if tc.expected != nil { 510 t.Errorf("testGroupPath(%v, %v) got unexpected error: %v", path, tc.groupName, err) 511 } 512 case tc.expected == nil: 513 t.Errorf("testGroupPath(%v, %v) failed to receive an error", path, tc.groupName) 514 default: 515 if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(gcs.Path{})); diff != "" { 516 t.Errorf("testGroupPath(%v, %v) got unexpected diff (-have, +want):\n%s", path, tc.groupName, diff) 517 } 518 } 519 }) 520 } 521 } 522 523 func jsonStarted(stamp int64) *fakeObject { 524 return &fakeObject{ 525 Data: jsonData(metadata.Started{Timestamp: stamp}), 526 } 527 } 528 529 func jsonFinished(stamp int64, passed bool, meta metadata.Metadata) *fakeObject { 530 return &fakeObject{ 531 Data: jsonData(metadata.Finished{ 532 Timestamp: &stamp, 533 Passed: &passed, 534 Metadata: meta, 535 }), 536 } 537 } 538 539 var ( 540 podInfoSuccessPodInfo = gcs.PodInfo{ 541 Pod: &core.Pod{ 542 Status: core.PodStatus{ 543 Phase: core.PodSucceeded, 544 }, 545 }, 546 } 547 podInfoSuccess = jsonPodInfo(podInfoSuccessPodInfo) 548 podInfoPassCell = cell{Result: statuspb.TestStatus_PASS} 549 podInfoMissingCell = cell{ 550 Result: statuspb.TestStatus_RUNNING, 551 Icon: "!", 552 Message: gcs.MissingPodInfo, 553 } 554 ) 555 556 func jsonPodInfo(podInfo gcs.PodInfo) *fakeObject { 557 return &fakeObject{Data: jsonData(podInfo)} 558 } 559 560 func mustGrid(grid *statepb.Grid) []byte { 561 buf, err := gcs.MarshalGrid(grid) 562 if err != nil { 563 panic(err) 564 } 565 return buf 566 } 567 568 func TestTruncateRunning(t *testing.T) { 569 now := float64(time.Now().UTC().Unix() * 1000) 570 floor := time.Now().Add(-72 * time.Hour) 571 ancient := float64(time.Now().Add(-74*time.Hour).UTC().Unix() * 1000) 572 cases := []struct { 573 name string 574 cols []inflatedColumn 575 expected func([]inflatedColumn) []inflatedColumn 576 }{ 577 { 578 name: "basically works", 579 }, 580 { 581 name: "keep everything (no Overall)", 582 cols: []inflatedColumn{ 583 { 584 Column: &statepb.Column{ 585 Build: "this", 586 Started: now, 587 }, 588 }, 589 { 590 Column: &statepb.Column{ 591 Build: "that", 592 Started: now, 593 }, 594 }, 595 { 596 Column: &statepb.Column{ 597 Build: "another", 598 Started: now, 599 }, 600 }, 601 }, 602 }, 603 { 604 name: "keep everything completed", 605 cols: []inflatedColumn{ 606 { 607 Column: &statepb.Column{ 608 Build: "passed", 609 Started: now, 610 }, 611 Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_PASS}}, 612 }, 613 { 614 Column: &statepb.Column{ 615 Build: "failed", 616 Started: now, 617 }, 618 Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_FAIL}}, 619 }, 620 }, 621 }, 622 { 623 name: "drop everything before oldest running", 624 cols: []inflatedColumn{ 625 { 626 Column: &statepb.Column{ 627 Build: "this1", 628 Started: now, 629 }, 630 }, 631 { 632 Column: &statepb.Column{ 633 Build: "this2", 634 Started: now, 635 }, 636 }, 637 { 638 Column: &statepb.Column{ 639 Build: "running1", 640 Started: now, 641 }, 642 Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_RUNNING}}, 643 }, 644 { 645 Column: &statepb.Column{ 646 Build: "this3", 647 Started: now, 648 }, 649 }, 650 { 651 Column: &statepb.Column{ 652 Build: "running2", 653 Started: now, 654 }, 655 Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_RUNNING}}, 656 }, 657 { 658 Column: &statepb.Column{ 659 Build: "this4", 660 Started: now, 661 }, 662 }, 663 { 664 Column: &statepb.Column{ 665 Build: "this5", 666 Started: now, 667 }, 668 }, 669 { 670 Column: &statepb.Column{ 671 Build: "this6", 672 Started: now, 673 }, 674 }, 675 { 676 Column: &statepb.Column{ 677 Build: "this7", 678 Started: now, 679 }, 680 }, 681 }, 682 expected: func(cols []inflatedColumn) []inflatedColumn { 683 return cols[5:] // this4 and earlier 684 }, 685 }, 686 { 687 name: "drop all as all are running", 688 cols: []inflatedColumn{ 689 { 690 Column: &statepb.Column{ 691 Build: "running1", 692 Started: now, 693 }, 694 Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_RUNNING}}, 695 }, 696 { 697 Column: &statepb.Column{ 698 Build: "running2", 699 Started: now, 700 }, 701 Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_RUNNING}}, 702 }, 703 }, 704 expected: func(cols []inflatedColumn) []inflatedColumn { 705 return cols[2:] 706 }, 707 }, 708 { 709 name: "drop running columns if any process is running", 710 cols: []InflatedColumn{ 711 { 712 Column: &statepb.Column{ 713 Build: "running", 714 Started: now, 715 }, 716 Cells: map[string]cell{ 717 "process1": {Result: statuspb.TestStatus_RUNNING}, 718 "process2": {Result: statuspb.TestStatus_RUNNING}, 719 }, 720 }, 721 { 722 Column: &statepb.Column{ 723 Build: "running-partially", 724 Started: now, 725 }, 726 Cells: map[string]cell{ 727 "process1": {Result: statuspb.TestStatus_RUNNING}, 728 "process2": {Result: statuspb.TestStatus_PASS}, 729 }, 730 }, 731 { 732 Column: &statepb.Column{ 733 Build: "ok", 734 Started: now, 735 }, 736 Cells: map[string]cell{ 737 "process1": {Result: statuspb.TestStatus_PASS}, 738 "process2": {Result: statuspb.TestStatus_PASS}, 739 }, 740 }, 741 }, 742 expected: func(cols []inflatedColumn) []inflatedColumn { 743 return cols[2:] 744 }, 745 }, 746 { 747 name: "ignore ancient running columns", 748 cols: []InflatedColumn{ 749 { 750 Column: &statepb.Column{ 751 Build: "recent-running", 752 Started: now, 753 }, 754 Cells: map[string]cell{"drop": {Result: statuspb.TestStatus_RUNNING}}, 755 }, 756 { 757 Column: &statepb.Column{ 758 Build: "recent-done", 759 Started: now - 1, 760 }, 761 Cells: map[string]cell{"keep": {Result: statuspb.TestStatus_PASS}}, 762 }, 763 764 { 765 Column: &statepb.Column{ 766 Build: "running-ancient", 767 Started: ancient, 768 }, 769 Cells: map[string]cell{"too-old-to-drop": {Result: statuspb.TestStatus_RUNNING}}, 770 }, 771 { 772 Column: &statepb.Column{ 773 Build: "ok", 774 Started: ancient - 1, 775 }, 776 Cells: map[string]cell{"also keep": {Result: statuspb.TestStatus_PASS}}, 777 }, 778 }, 779 expected: func(cols []inflatedColumn) []inflatedColumn { 780 return cols[1:] 781 }, 782 }, 783 } 784 785 for _, tc := range cases { 786 t.Run(tc.name, func(t *testing.T) { 787 actual := truncateRunning(tc.cols, floor) 788 expected := tc.cols 789 if tc.expected != nil { 790 expected = tc.expected(expected) 791 } 792 if diff := cmp.Diff(actual, expected, protocmp.Transform()); diff != "" { 793 t.Errorf("truncateRunning() got unexpected diff:\n%s", diff) 794 } 795 }) 796 } 797 } 798 799 func TestListBuilds(t *testing.T) { 800 cases := []struct { 801 name string 802 since string 803 client fakeLister 804 paths []gcs.Path 805 expected []gcs.Build 806 err bool 807 }{ 808 { 809 name: "basically works", 810 }, 811 { 812 name: "list err", 813 client: fakeLister{ 814 newPathOrDie("gs://prefix/job/"): fakeIterator{ 815 Objects: []storage.ObjectAttrs{ 816 { 817 Prefix: "job/1/", 818 }, 819 { 820 Prefix: "job/10/", 821 }, 822 { 823 Prefix: "job/2/", 824 }, 825 }, 826 Err: 1, 827 }, 828 }, 829 paths: []gcs.Path{ 830 newPathOrDie("gs://prefix/job/"), 831 }, 832 err: true, 833 }, 834 { 835 name: "bucket err", 836 client: fakeLister{ 837 newPathOrDie("gs://prefix/job/"): fakeIterator{ 838 Objects: []storage.ObjectAttrs{ 839 { 840 Prefix: "job/1/", 841 }, 842 { 843 Prefix: "job/10/", 844 }, 845 { 846 Prefix: "job/2/", 847 }, 848 }, 849 ErrOpen: storage.ErrBucketNotExist, 850 }, 851 }, 852 paths: []gcs.Path{ 853 newPathOrDie("gs://prefix/job/"), 854 }, 855 err: true, 856 }, 857 { 858 name: "list stuff correctly", 859 client: fakeLister{ 860 newPathOrDie("gs://prefix/job/"): fakeIterator{ 861 Objects: []storage.ObjectAttrs{ 862 { 863 Prefix: "job/1/", 864 }, 865 { 866 Prefix: "job/10/", 867 }, 868 { 869 Prefix: "job/2/", 870 }, 871 }, 872 }, 873 }, 874 paths: []gcs.Path{ 875 newPathOrDie("gs://prefix/job/"), 876 }, 877 expected: []gcs.Build{ 878 { 879 Path: newPathOrDie("gs://prefix/job/10/"), 880 }, 881 { 882 Path: newPathOrDie("gs://prefix/job/2/"), 883 }, 884 { 885 Path: newPathOrDie("gs://prefix/job/1/"), 886 }, 887 }, 888 }, 889 { 890 name: "list offsets correctly", 891 since: "3", 892 client: fakeLister{ 893 newPathOrDie("gs://prefix/job/"): fakeIterator{ 894 Objects: []storage.ObjectAttrs{ 895 { 896 Prefix: "job/1/", 897 }, 898 { 899 Prefix: "job/10/", 900 }, 901 { 902 Prefix: "job/2/", 903 }, 904 { 905 Prefix: "job/3/", 906 }, 907 { 908 Prefix: "job/4/", 909 }, 910 }, 911 }, 912 }, 913 paths: []gcs.Path{ 914 newPathOrDie("gs://prefix/job/"), 915 }, 916 expected: []gcs.Build{ 917 { 918 Path: newPathOrDie("gs://prefix/job/10/"), 919 }, 920 { 921 Path: newPathOrDie("gs://prefix/job/4/"), 922 }, 923 }, 924 }, 925 { 926 name: "collate stuff correctly", 927 client: fakeLister{ 928 newPathOrDie("gs://prefix/job/"): fakeIterator{ 929 Objects: []storage.ObjectAttrs{ 930 { 931 Prefix: "job/1/", 932 }, 933 { 934 Prefix: "job/10/", 935 }, 936 { 937 Prefix: "job/3/", 938 }, 939 }, 940 }, 941 newPathOrDie("gs://other-prefix/presubmit-job/"): fakeIterator{ 942 Objects: []storage.ObjectAttrs{ 943 { 944 Name: "job/2", 945 Metadata: map[string]string{ 946 "link": "gs://foo/bar333", // intentionally larger than job 20 and 4 947 }, 948 }, 949 { 950 Name: "job/20", 951 Metadata: map[string]string{ 952 "link": "gs://foo/bar222", 953 }, 954 }, 955 { 956 957 Name: "job/4", 958 Metadata: map[string]string{ 959 "link": "gs://foo/bar111", 960 }, 961 }, 962 }, 963 }, 964 }, 965 paths: []gcs.Path{ 966 newPathOrDie("gs://prefix/job/"), 967 newPathOrDie("gs://other-prefix/presubmit-job/"), 968 }, 969 expected: []gcs.Build{ 970 { 971 Path: newPathOrDie("gs://foo/bar222/"), 972 // baseName: 20 973 }, 974 { 975 Path: newPathOrDie("gs://prefix/job/10/"), 976 }, 977 { 978 Path: newPathOrDie("gs://foo/bar111/"), 979 // baseName: 4 980 }, 981 { 982 Path: newPathOrDie("gs://prefix/job/3/"), 983 }, 984 { 985 Path: newPathOrDie("gs://foo/bar333/"), 986 // baseName: 2 987 }, 988 { 989 Path: newPathOrDie("gs://prefix/job/1/"), 990 }, 991 }, 992 }, 993 { 994 name: "collated offsets work correctly", 995 since: "5", // drop 4 3 2 1, keep 20, 10 996 client: fakeLister{ 997 newPathOrDie("gs://prefix/job/"): fakeIterator{ 998 Objects: []storage.ObjectAttrs{ 999 { 1000 Prefix: "job/1/", 1001 }, 1002 { 1003 Prefix: "job/10/", 1004 }, 1005 { 1006 Prefix: "job/3/", 1007 }, 1008 }, 1009 }, 1010 newPathOrDie("gs://other-prefix/presubmit-job/"): fakeIterator{ 1011 Objects: []storage.ObjectAttrs{ 1012 { 1013 Name: "job/2", 1014 Metadata: map[string]string{ 1015 "link": "gs://foo/bar333", // intentionally larger than job 20 and 4 1016 }, 1017 }, 1018 { 1019 Name: "job/20", 1020 Metadata: map[string]string{ 1021 "link": "gs://foo/bar222", 1022 }, 1023 }, 1024 { 1025 1026 Name: "job/4", 1027 Metadata: map[string]string{ 1028 "link": "gs://foo/bar111", 1029 }, 1030 }, 1031 }, 1032 }, 1033 }, 1034 paths: []gcs.Path{ 1035 newPathOrDie("gs://prefix/job/"), 1036 newPathOrDie("gs://other-prefix/presubmit-job/"), 1037 }, 1038 expected: []gcs.Build{ 1039 { 1040 Path: newPathOrDie("gs://foo/bar222/"), 1041 // baseName: 20 1042 }, 1043 { 1044 Path: newPathOrDie("gs://prefix/job/10/"), 1045 }, 1046 }, 1047 }, 1048 } 1049 1050 compareBuilds := cmp.Comparer(func(x, y gcs.Build) bool { 1051 return x.String() == y.String() 1052 }) 1053 ctx := context.Background() 1054 for _, tc := range cases { 1055 t.Run(tc.name, func(t *testing.T) { 1056 actual, err := listBuilds(ctx, tc.client, tc.since, tc.paths...) 1057 switch { 1058 case err != nil: 1059 if !tc.err { 1060 t.Errorf("listBuilds() got unexpected error: %v", err) 1061 } 1062 case tc.err: 1063 t.Errorf("listBuilds() failed to return an error") 1064 default: 1065 if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(gcs.Path{}), compareBuilds); diff != "" { 1066 t.Errorf("listBuilds() got unexpected diff (-have, +want):\n%s", diff) 1067 } 1068 } 1069 }) 1070 } 1071 } 1072 1073 func TestInflateDropAppend(t *testing.T) { 1074 const dayAgo = 60 * 60 * 24 1075 now := time.Now().Unix() 1076 uploadPath := newPathOrDie("gs://fake/upload/location") 1077 defaultTimeout := 5 * time.Minute 1078 // a simple ColumnReader that parses fakeBuilds 1079 fakeColReader := func(builds []fakeBuild) ColumnReader { 1080 return func(ctx context.Context, _ logrus.FieldLogger, _ *configpb.TestGroup, _ []InflatedColumn, _ time.Time, receivers chan<- InflatedColumn) error { 1081 ctx, cancel := context.WithCancel(ctx) 1082 defer cancel() // do not leak go routines 1083 for i := len(builds) - 1; i >= 0; i-- { 1084 b := builds[i] 1085 started := metadata.Started{} 1086 if err := json.Unmarshal([]byte(b.started.Data), &started); err != nil { 1087 return err 1088 } 1089 col := InflatedColumn{ 1090 Column: &statepb.Column{ 1091 Build: b.id, 1092 Started: float64(started.Timestamp * 1000), 1093 Hint: b.id, 1094 }, 1095 Cells: map[string]Cell{}, 1096 } 1097 for _, cell := range b.passed { 1098 col.Cells[cell] = Cell{Result: statuspb.TestStatus_PASS} 1099 } 1100 for _, cell := range b.failed { 1101 col.Cells[cell] = Cell{Result: statuspb.TestStatus_FAIL} 1102 } 1103 select { 1104 case <-ctx.Done(): 1105 return ctx.Err() 1106 case receivers <- col: 1107 } 1108 } 1109 return nil 1110 } 1111 } 1112 cases := []struct { 1113 name string 1114 ctx context.Context 1115 builds []fakeBuild 1116 group *configpb.TestGroup 1117 concurrency int 1118 skipWrite bool 1119 colReader func(builds []fakeBuild) ColumnReader 1120 reprocess time.Duration 1121 groupTimeout *time.Duration 1122 buildTimeout *time.Duration 1123 current *fake.Object 1124 expected *fakeUpload 1125 err bool 1126 }{ 1127 { 1128 name: "basically works", 1129 group: &configpb.TestGroup{ 1130 GcsPrefix: "bucket/path/to/build/", 1131 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 1132 { 1133 ConfigurationValue: "Commit", 1134 }, 1135 }, 1136 }, 1137 builds: []fakeBuild{ 1138 { 1139 id: "99", 1140 started: jsonStarted(now + 99), 1141 }, 1142 { 1143 id: "80", 1144 started: jsonStarted(now + 80), 1145 podInfo: podInfoSuccess, 1146 finished: jsonFinished(now+81, true, metadata.Metadata{ 1147 metadata.JobVersion: "build80", 1148 }), 1149 passed: []string{"good1", "good2", "flaky"}, 1150 }, 1151 { 1152 id: "50", 1153 started: jsonStarted(now + 50), 1154 podInfo: podInfoSuccess, 1155 finished: jsonFinished(now+51, false, metadata.Metadata{ 1156 metadata.JobVersion: "build50", 1157 }), 1158 passed: []string{"good1", "good2"}, 1159 failed: []string{"flaky"}, 1160 }, 1161 { 1162 id: "10", 1163 started: jsonStarted(now + 10), 1164 podInfo: podInfoSuccess, 1165 finished: jsonFinished(now+11, true, metadata.Metadata{ 1166 metadata.JobVersion: "build10", 1167 }), 1168 passed: []string{"good1", "good2", "flaky"}, 1169 }, 1170 }, 1171 expected: &fakeUpload{ 1172 Buf: mustGrid(&statepb.Grid{ 1173 Columns: []*statepb.Column{ 1174 { 1175 Build: "99", 1176 Hint: "99", 1177 Started: float64(now+99) * 1000, 1178 Extra: []string{""}, 1179 }, 1180 { 1181 Build: "80", 1182 Hint: "80", 1183 Started: float64(now+80) * 1000, 1184 Extra: []string{"build80"}, 1185 }, 1186 { 1187 Build: "50", 1188 Hint: "50", 1189 Started: float64(now+50) * 1000, 1190 Extra: []string{"build50"}, 1191 }, 1192 { 1193 Build: "10", 1194 Hint: "10", 1195 Started: float64(now+10) * 1000, 1196 Extra: []string{"build10"}, 1197 }, 1198 }, 1199 Rows: []*statepb.Row{ 1200 setupRow( 1201 &statepb.Row{ 1202 Name: "build." + overallRow, 1203 Id: "build." + overallRow, 1204 }, 1205 cell{ 1206 Result: statuspb.TestStatus_RUNNING, 1207 Message: "Build still running...", 1208 Icon: "R", 1209 }, 1210 cell{ 1211 Result: statuspb.TestStatus_PASS, 1212 Metrics: setElapsed(nil, 1), 1213 }, 1214 cell{ 1215 Result: statuspb.TestStatus_FAIL, 1216 Metrics: setElapsed(nil, 1), 1217 }, 1218 cell{ 1219 Result: statuspb.TestStatus_PASS, 1220 Metrics: setElapsed(nil, 1), 1221 }, 1222 ), 1223 setupRow( 1224 &statepb.Row{ 1225 Name: "build." + podInfoRow, 1226 Id: "build." + podInfoRow, 1227 }, 1228 cell{Result: statuspb.TestStatus_NO_RESULT}, 1229 podInfoPassCell, 1230 podInfoPassCell, 1231 podInfoPassCell, 1232 ), 1233 setupRow( 1234 &statepb.Row{ 1235 Name: "flaky", 1236 Id: "flaky", 1237 }, 1238 cell{Result: statuspb.TestStatus_NO_RESULT}, 1239 cell{Result: statuspb.TestStatus_PASS}, 1240 cell{ 1241 Result: statuspb.TestStatus_FAIL, 1242 Message: "flaky", 1243 Icon: "F", 1244 }, 1245 cell{Result: statuspb.TestStatus_PASS}, 1246 ), 1247 setupRow( 1248 &statepb.Row{ 1249 Name: "good1", 1250 Id: "good1", 1251 }, 1252 cell{Result: statuspb.TestStatus_NO_RESULT}, 1253 cell{Result: statuspb.TestStatus_PASS}, 1254 cell{Result: statuspb.TestStatus_PASS}, 1255 cell{Result: statuspb.TestStatus_PASS}, 1256 ), 1257 setupRow( 1258 &statepb.Row{ 1259 Name: "good2", 1260 Id: "good2", 1261 }, 1262 cell{Result: statuspb.TestStatus_NO_RESULT}, 1263 cell{Result: statuspb.TestStatus_PASS}, 1264 cell{Result: statuspb.TestStatus_PASS}, 1265 cell{Result: statuspb.TestStatus_PASS}, 1266 ), 1267 }, 1268 }), 1269 CacheControl: "no-cache", 1270 WorldRead: gcs.DefaultACL, 1271 Generation: 1, 1272 }, 1273 }, 1274 { 1275 name: "do not write when requested", 1276 skipWrite: true, 1277 group: &configpb.TestGroup{ 1278 GcsPrefix: "bucket/path/to/build/", 1279 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 1280 { 1281 ConfigurationValue: "Commit", 1282 }, 1283 }, 1284 }, 1285 builds: []fakeBuild{ 1286 { 1287 id: "99", 1288 started: jsonStarted(now + 99), 1289 }, 1290 { 1291 id: "80", 1292 started: jsonStarted(now + 80), 1293 finished: jsonFinished(now+81, true, metadata.Metadata{ 1294 metadata.JobVersion: "build80", 1295 }), 1296 passed: []string{"good1", "good2", "flaky"}, 1297 }, 1298 { 1299 id: "50", 1300 started: jsonStarted(now + 50), 1301 finished: jsonFinished(now+51, false, metadata.Metadata{ 1302 metadata.JobVersion: "build50", 1303 }), 1304 passed: []string{"good1", "good2"}, 1305 failed: []string{"flaky"}, 1306 }, 1307 { 1308 id: "10", 1309 started: jsonStarted(now + 10), 1310 finished: jsonFinished(now+11, true, metadata.Metadata{ 1311 metadata.JobVersion: "build10", 1312 }), 1313 passed: []string{"good1", "good2", "flaky"}, 1314 }, 1315 }, 1316 }, 1317 { 1318 name: "recent", // keep columns past the reprocess boundary 1319 group: &configpb.TestGroup{ 1320 GcsPrefix: "bucket/path/to/build/", 1321 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 1322 { 1323 ConfigurationValue: "Commit", 1324 }, 1325 }, 1326 }, 1327 reprocess: 10 * time.Second, 1328 builds: []fakeBuild{ 1329 { 1330 id: "current", 1331 started: jsonStarted(now), 1332 }, 1333 }, 1334 current: &fake.Object{ 1335 Data: string(mustGrid(&statepb.Grid{ 1336 Columns: []*statepb.Column{ 1337 { 1338 Build: "current", 1339 Hint: "should reprocess", 1340 Started: float64(now * 1000), 1341 Extra: []string{""}, 1342 }, 1343 { 1344 Build: "near boundary", 1345 Hint: "1 should disappear", 1346 Started: float64(now-7) * 1000, // allow for 2s of clock drift 1347 Extra: []string{""}, 1348 }, 1349 { 1350 Build: "past boundary", 1351 Hint: "boundary+999", 1352 Started: float64(now-9)*1000 - 1, 1353 Extra: []string{"keep"}, 1354 }, 1355 }, 1356 Rows: []*statepb.Row{ 1357 setupRow( 1358 &statepb.Row{ 1359 Name: "build." + overallRow, 1360 Id: "build." + overallRow, 1361 }, 1362 cell{ 1363 Result: statuspb.TestStatus_PASS, 1364 Message: "old data", 1365 Icon: "should reprocess", 1366 }, 1367 cell{ 1368 Result: statuspb.TestStatus_FAIL, 1369 Message: "delete me", 1370 Icon: "me too", 1371 }, 1372 cell{ 1373 Result: statuspb.TestStatus_FLAKY, 1374 Message: "keep me", 1375 Icon: "yes stay", 1376 }, 1377 ), 1378 }, 1379 })), 1380 }, 1381 expected: &fakeUpload{ 1382 Buf: mustGrid(&statepb.Grid{ 1383 Columns: []*statepb.Column{ 1384 { 1385 Build: "current", 1386 Hint: "current", 1387 Started: float64(now) * 1000, 1388 Extra: []string{""}, 1389 }, 1390 { 1391 Build: "past boundary", 1392 Hint: "boundary+999", 1393 Started: float64(now-9)*1000 - 1, 1394 Extra: []string{"keep"}, 1395 }, 1396 }, 1397 Rows: []*statepb.Row{ 1398 setupRow( 1399 &statepb.Row{ 1400 Name: "build." + overallRow, 1401 Id: "build." + overallRow, 1402 }, 1403 cell{ 1404 Result: statuspb.TestStatus_RUNNING, 1405 Message: "Build still running...", 1406 Icon: "R", 1407 }, 1408 cell{ 1409 Result: statuspb.TestStatus_FLAKY, 1410 Message: "keep me", 1411 Icon: "yes stay", 1412 }, 1413 ), 1414 }, 1415 }), 1416 CacheControl: "no-cache", 1417 WorldRead: gcs.DefaultACL, 1418 Generation: 1, 1419 }, 1420 }, 1421 { 1422 name: "skip reprocess", 1423 group: &configpb.TestGroup{ 1424 GcsPrefix: "bucket/path/to/build/", 1425 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 1426 { 1427 ConfigurationValue: "Commit", 1428 }, 1429 }, 1430 }, 1431 reprocess: 10 * time.Second, 1432 builds: []fakeBuild{ 1433 { 1434 id: "current", 1435 started: jsonStarted(now), 1436 }, 1437 }, 1438 current: &fake.Object{ 1439 Data: string(mustGrid(&statepb.Grid{ 1440 Columns: []*statepb.Column{ 1441 { 1442 Build: "current", 1443 Hint: "should reprocess", 1444 Started: float64(now * 1000), 1445 Extra: []string{""}, 1446 }, 1447 { 1448 Build: "at boundary", 1449 Hint: "1 should disappear", 1450 Started: float64(now-9) * 1000, 1451 Extra: []string{""}, 1452 }, 1453 { 1454 Build: "past boundary", 1455 Hint: "1 running", 1456 Started: float64(now-40) * 1000, 1457 Extra: []string{""}, 1458 }, 1459 { 1460 Build: "paster boundary", 1461 Hint: "1 maybe fix", 1462 Started: float64(now-50) * 1000, 1463 Extra: []string{""}, 1464 }, 1465 { 1466 Build: "pastest boundary", 1467 Hint: "1 oldest", 1468 Started: float64(now-60) * 1000, 1469 Extra: []string{"keep"}, 1470 }, 1471 }, 1472 Rows: []*statepb.Row{ 1473 setupRow( 1474 &statepb.Row{ 1475 Name: "build." + overallRow, 1476 Id: "build." + overallRow, 1477 }, 1478 cell{ 1479 Result: statuspb.TestStatus_PASS, 1480 Message: "old data", 1481 Icon: "should reprocess", 1482 }, 1483 cell{ 1484 Result: statuspb.TestStatus_FAIL, 1485 Message: "delete me", 1486 Icon: "me too", 1487 }, 1488 cell{ 1489 Result: statuspb.TestStatus_RUNNING, 1490 Message: "delete me", 1491 Icon: "me too", 1492 }, 1493 cell{ 1494 Result: statuspb.TestStatus_FLAKY, 1495 Message: "maybe reprocess", 1496 Icon: "?", 1497 }, 1498 cell{ 1499 Result: statuspb.TestStatus_PASS, 1500 Message: "keep me", 1501 Icon: "yes stay", 1502 }, 1503 ), 1504 }, 1505 })), 1506 }, 1507 expected: &fakeUpload{ 1508 Buf: mustGrid(&statepb.Grid{ 1509 Columns: []*statepb.Column{ 1510 { 1511 Build: "current", 1512 Hint: "current", 1513 Started: float64(now) * 1000, 1514 Extra: []string{""}, 1515 }, 1516 { 1517 Build: "paster boundary", 1518 Hint: "1 maybe fix", 1519 Started: float64(now-50) * 1000, 1520 Extra: []string{""}, 1521 }, 1522 { 1523 Build: "pastest boundary", 1524 Hint: "1 oldest", 1525 Started: float64(now-60) * 1000, 1526 Extra: []string{"keep"}, 1527 }, 1528 }, 1529 Rows: []*statepb.Row{ 1530 setupRow( 1531 &statepb.Row{ 1532 Name: "build." + overallRow, 1533 Id: "build." + overallRow, 1534 }, 1535 cell{ 1536 Result: statuspb.TestStatus_RUNNING, 1537 Message: "Build still running...", 1538 Icon: "R", 1539 }, 1540 cell{ 1541 Result: statuspb.TestStatus_FLAKY, 1542 Message: "maybe reprocess", 1543 Icon: "?", 1544 }, 1545 cell{ 1546 Result: statuspb.TestStatus_PASS, 1547 Message: "keep me", 1548 Icon: "yes stay", 1549 }, 1550 ), 1551 }, 1552 }), 1553 CacheControl: "no-cache", 1554 WorldRead: gcs.DefaultACL, 1555 Generation: 1, 1556 }, 1557 }, 1558 { 1559 name: "reprocess", 1560 group: &configpb.TestGroup{ 1561 GcsPrefix: "bucket/path/to/build/", 1562 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 1563 { 1564 ConfigurationValue: "Commit", 1565 }, 1566 }, 1567 DaysOfResults: 30, 1568 }, 1569 reprocess: 10 * time.Second, 1570 builds: []fakeBuild{ 1571 { 1572 id: "current", 1573 started: jsonStarted(now), 1574 }, 1575 }, 1576 current: &fake.Object{ 1577 Data: string(mustGrid(&statepb.Grid{ 1578 Config: &configpb.TestGroup{ 1579 GcsPrefix: "bucket/path/to/build/", 1580 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 1581 { 1582 ConfigurationValue: "Commit", 1583 }, 1584 }, 1585 DaysOfResults: 20, 1586 }, 1587 Columns: []*statepb.Column{ 1588 { 1589 Build: "current", 1590 Hint: "should reprocess", 1591 Started: float64(now * 1000), 1592 Extra: []string{""}, 1593 }, 1594 { 1595 Build: "at boundary", 1596 Hint: "1 should disappear", 1597 Started: float64(now-9) * 1000, 1598 Extra: []string{""}, 1599 }, 1600 { 1601 Build: "past boundary", 1602 Hint: "1 running", 1603 Started: float64(now-40) * 1000, 1604 Extra: []string{""}, 1605 }, 1606 { 1607 Build: "paster boundary", 1608 Hint: "1 maybe fix", 1609 Started: float64(now-50-dayAgo) * 1000, 1610 Extra: []string{""}, 1611 }, 1612 { 1613 Build: "pastest boundary", 1614 Hint: "1 oldest", 1615 Started: float64(now-60-10*dayAgo) * 1000, 1616 Extra: []string{"keep"}, 1617 }, 1618 }, 1619 Rows: []*statepb.Row{ 1620 setupRow( 1621 &statepb.Row{ 1622 Name: "build." + overallRow, 1623 Id: "build." + overallRow, 1624 }, 1625 cell{ 1626 Result: statuspb.TestStatus_PASS, 1627 Message: "old data", 1628 Icon: "should reprocess", 1629 }, 1630 cell{ 1631 Result: statuspb.TestStatus_FAIL, 1632 Message: "delete me", 1633 Icon: "me too", 1634 }, 1635 cell{ 1636 Result: statuspb.TestStatus_RUNNING, 1637 Message: "delete me", 1638 Icon: "me too", 1639 }, 1640 cell{ 1641 Result: statuspb.TestStatus_FLAKY, 1642 Message: "maybe reprocess", 1643 Icon: "?", 1644 }, 1645 cell{ 1646 Result: statuspb.TestStatus_PASS, 1647 Message: "keep me", 1648 Icon: "yes stay", 1649 }, 1650 ), 1651 }, 1652 })), 1653 }, 1654 expected: &fakeUpload{ 1655 Buf: mustGrid(&statepb.Grid{ 1656 Columns: []*statepb.Column{ 1657 { 1658 Build: "current", 1659 Hint: "current", 1660 Started: float64(now) * 1000, 1661 Extra: []string{""}, 1662 }, 1663 { 1664 Build: "pastest boundary", 1665 Hint: "1 oldest", 1666 Started: float64(now-60-10*dayAgo) * 1000, 1667 Extra: []string{"keep"}, 1668 }, 1669 }, 1670 Rows: []*statepb.Row{ 1671 setupRow( 1672 &statepb.Row{ 1673 Name: "build." + overallRow, 1674 Id: "build." + overallRow, 1675 }, 1676 cell{ 1677 Result: statuspb.TestStatus_RUNNING, 1678 Message: "Build still running...", 1679 Icon: "R", 1680 }, 1681 cell{ 1682 Result: statuspb.TestStatus_PASS, 1683 Message: "keep me", 1684 Icon: "yes stay", 1685 }, 1686 ), 1687 }, 1688 }), 1689 CacheControl: "no-cache", 1690 WorldRead: gcs.DefaultACL, 1691 Generation: 1, 1692 }, 1693 }, 1694 { 1695 // short reprocessing time depends on our reprocessing running columns outside this timeframe. 1696 name: "running", // reprocess everything at least as new as the running column 1697 group: &configpb.TestGroup{ 1698 GcsPrefix: "bucket/path/to/build/", 1699 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 1700 { 1701 ConfigurationValue: "Commit", 1702 }, 1703 }, 1704 }, 1705 reprocess: 10 * time.Second, 1706 builds: []fakeBuild{ 1707 { 1708 id: "current", 1709 started: jsonStarted(now), 1710 }, 1711 }, 1712 current: &fake.Object{ 1713 Data: string(mustGrid(&statepb.Grid{ 1714 Columns: []*statepb.Column{ 1715 { 1716 Build: "current", 1717 Hint: "should reprocess", 1718 Started: float64(now * 1000), 1719 Extra: []string{""}, 1720 }, 1721 { 1722 Build: "at boundary", 1723 Hint: "1 should disappear", 1724 Started: float64(now-9) * 1000, 1725 Extra: []string{""}, 1726 }, 1727 { 1728 Build: "past boundary", 1729 Hint: "1 done but still reprocess", 1730 Started: float64(now-40) * 1000, 1731 Extra: []string{""}, 1732 }, 1733 { 1734 Build: "paster boundary", 1735 Hint: "1 running", 1736 Started: float64(now-50) * 1000, 1737 Extra: []string{""}, 1738 }, 1739 { 1740 Build: "pastest boundary", 1741 Hint: "1 oldest", 1742 Started: float64(now-60) * 1000, 1743 Extra: []string{"keep"}, 1744 }, 1745 }, 1746 Rows: []*statepb.Row{ 1747 setupRow( 1748 &statepb.Row{ 1749 Name: "build." + overallRow, 1750 Id: "build." + overallRow, 1751 }, 1752 cell{ 1753 Result: statuspb.TestStatus_PASS, 1754 Message: "old data", 1755 Icon: "should reprocess", 1756 }, 1757 cell{ 1758 Result: statuspb.TestStatus_FAIL, 1759 Message: "delete me", 1760 Icon: "me too", 1761 }, 1762 cell{ 1763 Result: statuspb.TestStatus_FAIL, 1764 Message: "delete me", 1765 Icon: "me too", 1766 }, 1767 cell{ 1768 Result: statuspb.TestStatus_RUNNING, 1769 Message: "delete me", 1770 Icon: "me too", 1771 }, 1772 cell{ 1773 Result: statuspb.TestStatus_FLAKY, 1774 Message: "keep me", 1775 Icon: "yes stay", 1776 }, 1777 ), 1778 }, 1779 })), 1780 }, 1781 expected: &fakeUpload{ 1782 Buf: mustGrid(&statepb.Grid{ 1783 Columns: []*statepb.Column{ 1784 { 1785 Build: "current", 1786 Hint: "current", 1787 Started: float64(now) * 1000, 1788 Extra: []string{""}, 1789 }, 1790 { 1791 Build: "pastest boundary", 1792 Hint: "1 oldest", 1793 Started: float64(now-60) * 1000, 1794 Extra: []string{"keep"}, 1795 }, 1796 }, 1797 Rows: []*statepb.Row{ 1798 setupRow( 1799 &statepb.Row{ 1800 Name: "build." + overallRow, 1801 Id: "build." + overallRow, 1802 }, 1803 cell{ 1804 Result: statuspb.TestStatus_RUNNING, 1805 Message: "Build still running...", 1806 Icon: "R", 1807 }, 1808 cell{ 1809 Result: statuspb.TestStatus_FLAKY, 1810 Message: "keep me", 1811 Icon: "yes stay", 1812 }, 1813 ), 1814 }, 1815 }), 1816 CacheControl: "no-cache", 1817 WorldRead: gcs.DefaultACL, 1818 Generation: 1, 1819 }, 1820 }, 1821 { 1822 name: "ignore empty", // ignore builds with no results. 1823 group: &configpb.TestGroup{ 1824 GcsPrefix: "bucket/path/to/build/", 1825 DisableProwjobAnalysis: true, 1826 }, 1827 reprocess: 10 * time.Second, 1828 colReader: fakeColReader, 1829 builds: []fakeBuild{ 1830 { 1831 id: "cool5", 1832 started: jsonStarted(now - 10), 1833 finished: jsonFinished(now-9, true, metadata.Metadata{}), 1834 passed: []string{"a-test"}, 1835 }, 1836 { 1837 id: "empty4", 1838 started: jsonStarted(now - 20), 1839 finished: jsonFinished(now-19, true, metadata.Metadata{}), 1840 }, 1841 { 1842 id: "empty3", 1843 started: jsonStarted(now - 30), 1844 finished: jsonFinished(now-29, true, metadata.Metadata{}), 1845 }, 1846 { 1847 id: "rad2", 1848 started: jsonStarted(now - 40), 1849 finished: jsonFinished(now-39, true, metadata.Metadata{}), 1850 passed: []string{"a-test"}, 1851 }, 1852 { 1853 id: "empty1", 1854 started: jsonStarted(now - 50), 1855 finished: jsonFinished(now-49, true, metadata.Metadata{}), 1856 }, 1857 }, 1858 current: &fake.Object{ 1859 Data: string(mustGrid(&statepb.Grid{ 1860 Columns: []*statepb.Column{}, 1861 Rows: []*statepb.Row{}, 1862 })), 1863 }, 1864 expected: &fakeUpload{ 1865 Buf: mustGrid(&statepb.Grid{ 1866 Columns: []*statepb.Column{ 1867 { 1868 Build: "cool5", 1869 Hint: "cool5", 1870 Started: float64((now - 10) * 1000), 1871 }, 1872 { 1873 Build: "rad2", 1874 Hint: "rad2", 1875 Started: float64((now - 40) * 1000), 1876 }, 1877 { 1878 Build: "", 1879 Hint: "empty4", 1880 Started: float64((now - 50) * 1000), 1881 }, 1882 }, 1883 Rows: []*statepb.Row{ 1884 setupRow( 1885 &statepb.Row{ 1886 Name: "a-test", 1887 Id: "a-test", 1888 }, 1889 cell{Result: statuspb.TestStatus_PASS}, 1890 cell{Result: statuspb.TestStatus_PASS}, 1891 cell{Result: statuspb.TestStatus_NO_RESULT}, 1892 ), 1893 }, 1894 }), 1895 CacheControl: "no-cache", 1896 WorldRead: gcs.DefaultACL, 1897 Generation: 1, 1898 }, 1899 }, 1900 { 1901 name: "ignore empty with old columns", // correctly group old and new empty columns 1902 group: &configpb.TestGroup{ 1903 GcsPrefix: "bucket/path/to/build/", 1904 DisableProwjobAnalysis: true, 1905 }, 1906 reprocess: 10 * time.Second, 1907 colReader: fakeColReader, 1908 builds: []fakeBuild{ 1909 { 1910 id: "empty9", 1911 started: jsonStarted(now - 10), 1912 finished: jsonFinished(now-9, true, metadata.Metadata{}), 1913 }, 1914 { 1915 id: "empty8", 1916 started: jsonStarted(now - 20), 1917 finished: jsonFinished(now-19, true, metadata.Metadata{}), 1918 }, 1919 { 1920 id: "wicked7", 1921 started: jsonStarted(now - 30), 1922 finished: jsonFinished(now-29, true, metadata.Metadata{}), 1923 passed: []string{"a-test"}, 1924 }, 1925 { 1926 id: "empty6", 1927 started: jsonStarted(now - 40), 1928 finished: jsonFinished(now-39, true, metadata.Metadata{}), 1929 }, 1930 }, 1931 current: &fake.Object{ 1932 Data: string(mustGrid(&statepb.Grid{ 1933 Columns: []*statepb.Column{ 1934 { 1935 Build: "cool5", 1936 Hint: "cool5", 1937 Started: float64((now - 50) * 1000), 1938 }, 1939 { 1940 Build: "rad2", 1941 Hint: "rad2", 1942 Started: float64((now - 80) * 1000), 1943 }, 1944 { 1945 Build: "", 1946 Name: "", 1947 Hint: "empty4", 1948 Started: float64((now - 90) * 1000), 1949 }, 1950 }, 1951 Rows: []*statepb.Row{ 1952 setupRow( 1953 &statepb.Row{ 1954 Name: "a-test", 1955 Id: "a-test", 1956 }, 1957 cell{Result: statuspb.TestStatus_PASS}, 1958 cell{Result: statuspb.TestStatus_PASS}, 1959 cell{Result: statuspb.TestStatus_NO_RESULT}, 1960 ), 1961 }, 1962 })), 1963 }, 1964 expected: &fakeUpload{ 1965 Buf: mustGrid(&statepb.Grid{ 1966 Columns: []*statepb.Column{ 1967 { 1968 Build: "wicked7", 1969 Hint: "wicked7", 1970 Started: float64((now - 30) * 1000), 1971 }, 1972 { 1973 Build: "cool5", 1974 Hint: "cool5", 1975 Started: float64((now - 50) * 1000), 1976 }, 1977 { 1978 Build: "rad2", 1979 Hint: "rad2", 1980 Started: float64((now - 80) * 1000), 1981 }, 1982 { 1983 Build: "", 1984 Name: "", 1985 Hint: "empty9", 1986 Started: float64((now - 90) * 1000), 1987 }, 1988 }, 1989 Rows: []*statepb.Row{ 1990 setupRow( 1991 &statepb.Row{ 1992 Name: "a-test", 1993 Id: "a-test", 1994 }, 1995 cell{Result: statuspb.TestStatus_PASS}, 1996 cell{Result: statuspb.TestStatus_PASS}, 1997 cell{Result: statuspb.TestStatus_PASS}, 1998 cell{Result: statuspb.TestStatus_NO_RESULT}, 1999 ), 2000 }, 2001 }), 2002 CacheControl: "no-cache", 2003 WorldRead: gcs.DefaultACL, 2004 Generation: 1, 2005 }, 2006 }, 2007 } 2008 2009 for _, tc := range cases { 2010 t.Run(tc.name, func(t *testing.T) { 2011 if tc.ctx == nil { 2012 tc.ctx = context.Background() 2013 } 2014 ctx, cancel := context.WithCancel(tc.ctx) 2015 defer cancel() 2016 2017 if tc.concurrency == 0 { 2018 tc.concurrency = 1 2019 } 2020 readResult := resultReaderPool(ctx, logrus.WithField("name", tc.name), tc.concurrency) 2021 if tc.groupTimeout == nil { 2022 tc.groupTimeout = &defaultTimeout 2023 } 2024 if tc.buildTimeout == nil { 2025 tc.buildTimeout = &defaultTimeout 2026 } 2027 2028 client := &fake.ConditionalClient{ 2029 UploadClient: fake.UploadClient{ 2030 Uploader: fakeUploader{}, 2031 Client: fakeClient{ 2032 Lister: fakeLister{}, 2033 Opener: fakeOpener{ 2034 Paths: map[gcs.Path]fake.Object{}, 2035 }, 2036 }, 2037 }, 2038 } 2039 2040 if tc.current != nil { 2041 client.Opener.Paths[uploadPath] = *tc.current 2042 } 2043 2044 buildsPath := newPathOrDie("gs://" + tc.group.GcsPrefix) 2045 fi := client.Lister[buildsPath] 2046 for _, build := range addBuilds(&client.Client, buildsPath, tc.builds...) { 2047 fi.Objects = append(fi.Objects, storage.ObjectAttrs{ 2048 Prefix: build.Path.Object(), 2049 }) 2050 } 2051 client.Lister[buildsPath] = fi 2052 2053 colReader := gcsColumnReader(client, *tc.buildTimeout, readResult, false) 2054 if tc.colReader != nil { 2055 colReader = tc.colReader(tc.builds) 2056 } 2057 _, err := InflateDropAppend( 2058 ctx, 2059 logrus.WithField("test", tc.name), 2060 client, 2061 tc.group, 2062 uploadPath, 2063 !tc.skipWrite, 2064 colReader, 2065 tc.reprocess, 2066 ) 2067 switch { 2068 case err != nil: 2069 if !tc.err { 2070 t.Errorf("InflateDropAppend() got unexpected error: %v", err) 2071 } 2072 case tc.err: 2073 t.Error("InflateDropAppend() failed to receive an error") 2074 default: 2075 expected := fakeUploader{} 2076 if tc.expected != nil { 2077 expected[uploadPath] = *tc.expected 2078 } 2079 actual := client.Uploader 2080 diff := cmp.Diff(expected, actual, cmp.AllowUnexported(gcs.Path{}, fakeUpload{}), protocmp.Transform()) 2081 if diff == "" { 2082 return 2083 } 2084 t.Logf("InflateDropAppend() generated a binary diff (-want +got):\n%s", diff) 2085 fakeDownloader := fakeOpener{ 2086 Paths: map[gcs.Path]fake.Object{ 2087 uploadPath: {Data: string(actual[uploadPath].Buf)}, 2088 }, 2089 } 2090 actualGrid, _, err := gcs.DownloadGrid(ctx, fakeDownloader, uploadPath) 2091 if err != nil { 2092 t.Errorf("actual gcs.DownloadGrid() got unexpected error: %v", err) 2093 } 2094 fakeDownloader.Paths[uploadPath] = fakeObject{Data: string(tc.expected.Buf)} 2095 expectedGrid, _, err := gcs.DownloadGrid(ctx, fakeDownloader, uploadPath) 2096 if err != nil { 2097 t.Errorf("expected gcs.DownloadGrid() got unexpected error: %v", err) 2098 } 2099 diff = cmp.Diff(expectedGrid, actualGrid, protocmp.Transform()) 2100 if diff == "" { 2101 return 2102 } 2103 t.Errorf("gcs.DownloadGrid() got unexpected diff (-want +got):\n%s", diff) 2104 } 2105 }) 2106 } 2107 } 2108 2109 func TestFormatStrftime(t *testing.T) { 2110 cases := []struct { 2111 name string 2112 want string 2113 }{ 2114 { 2115 name: "basically works", 2116 want: "basically works", 2117 }, 2118 { 2119 name: "Mon Jan 2 15:04:05", 2120 want: "Mon Jan 2 15:04:05", 2121 }, 2122 { 2123 name: "python am/pm: %p", 2124 want: "python am/pm: PM", 2125 }, 2126 { 2127 name: "python year: %Y", 2128 want: "python year: 2006", 2129 }, 2130 { 2131 name: "python short year: %y", 2132 want: "python short year: 06", 2133 }, 2134 { 2135 name: "python month: %m", 2136 want: "python month: 01", 2137 }, 2138 { 2139 name: "python date: %d", 2140 want: "python date: 02", 2141 }, 2142 { 2143 name: "python 24hr: %H", 2144 want: "python 24hr: 15", 2145 }, 2146 { 2147 name: "python minutes: %M", 2148 want: "python minutes: 04", 2149 }, 2150 { 2151 name: "python seconds: %S", 2152 want: "python seconds: 05", 2153 }, 2154 } 2155 2156 for _, tc := range cases { 2157 t.Run(tc.name, func(t *testing.T) { 2158 if got := FormatStrftime(tc.name); got != tc.want { 2159 t.Errorf("formatStrftime(%q) got %q want %q", tc.name, got, tc.want) 2160 } 2161 }) 2162 } 2163 2164 } 2165 2166 func TestTruncateGrid(t *testing.T) { 2167 addRows := func(n int, skel *Cell) map[string]Cell { 2168 if skel == nil { 2169 skel = &Cell{} 2170 } 2171 out := make(map[string]Cell, n) 2172 for i := 0; i < n; i++ { 2173 skel.ID = fmt.Sprintf("row-%d", i) 2174 out[skel.ID] = *skel 2175 } 2176 return out 2177 } 2178 2179 addCols := func(rows ...map[string]Cell) []InflatedColumn { 2180 out := make([]InflatedColumn, 0, len(rows)) 2181 for i, cells := range rows { 2182 id := fmt.Sprintf("col-%d", i) 2183 out = append(out, InflatedColumn{ 2184 Column: &statepb.Column{ 2185 Name: id, 2186 Build: id, 2187 }, 2188 Cells: cells, 2189 }) 2190 } 2191 return out 2192 } 2193 2194 cases := []struct { 2195 name string 2196 cols []InflatedColumn 2197 ceiling int 2198 want []InflatedColumn 2199 }{ 2200 { 2201 name: "basically works", 2202 }, 2203 { 2204 name: "keep first two cols", 2205 cols: addCols( 2206 addRows(1000, nil), 2207 addRows(1000, nil), 2208 ), 2209 want: addCols( 2210 addRows(1000, nil), 2211 addRows(1000, nil), 2212 ), 2213 }, 2214 { 2215 name: "shrink after second column", 2216 cols: addCols( 2217 addRows(1000, nil), 2218 addRows(1000, nil), 2219 addRows(1000, nil), 2220 addRows(1000, nil), 2221 addRows(1000, nil), 2222 ), 2223 want: addCols( 2224 addRows(1000, nil), 2225 addRows(1000, nil), 2226 ), 2227 }, 2228 { 2229 name: "honor ceiling", 2230 cols: addCols( 2231 addRows(1000, nil), 2232 addRows(1000, nil), 2233 addRows(1000, nil), 2234 addRows(1, nil), 2235 addRows(1000, nil), 2236 ), 2237 ceiling: 3001, 2238 want: addCols( 2239 addRows(1000, nil), 2240 addRows(1000, nil), 2241 addRows(1000, nil), 2242 addRows(1, nil), 2243 ), 2244 }, 2245 } 2246 2247 for _, tc := range cases { 2248 t.Run(tc.name, func(t *testing.T) { 2249 got := truncateGrid(tc.cols, tc.ceiling) 2250 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 2251 t.Errorf("truncateGrid() got unexpected diff (-want +got):\n%s", diff) 2252 } 2253 }) 2254 } 2255 } 2256 2257 func TestReprocessColumn(t *testing.T) { 2258 2259 now := time.Now() 2260 2261 cases := []struct { 2262 name string 2263 old *statepb.Grid 2264 cfg *configpb.TestGroup 2265 when time.Time 2266 want *InflatedColumn 2267 }{ 2268 { 2269 name: "empty", 2270 old: &statepb.Grid{}, 2271 }, 2272 { 2273 name: "same", 2274 old: &statepb.Grid{ 2275 Config: &configpb.TestGroup{Name: "same"}, 2276 }, 2277 cfg: &configpb.TestGroup{Name: "same"}, 2278 }, 2279 { 2280 name: "changed", 2281 old: &statepb.Grid{ 2282 Config: &configpb.TestGroup{Name: "old"}, 2283 }, 2284 cfg: &configpb.TestGroup{Name: "new"}, 2285 when: now, 2286 want: &InflatedColumn{ 2287 Column: &statepb.Column{ 2288 Started: float64(now.Unix() * 1000), 2289 }, 2290 Cells: map[string]Cell{ 2291 "reprocess": { 2292 Result: statuspb.TestStatus_RUNNING, 2293 }, 2294 }, 2295 }, 2296 }, 2297 } 2298 2299 for _, tc := range cases { 2300 t.Run(tc.name, func(t *testing.T) { 2301 got := reprocessColumn(logrus.WithField("name", tc.name), tc.old, tc.cfg, tc.when) 2302 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 2303 t.Errorf("reprocessColumn() got unexpected diff (-want +got):\n%s", diff) 2304 } 2305 }) 2306 } 2307 } 2308 2309 func Test_ShrinkGridInline(t *testing.T) { 2310 cases := []struct { 2311 name string 2312 ctx context.Context 2313 tg *configpb.TestGroup 2314 cols []InflatedColumn 2315 issues map[string][]string 2316 ceiling int 2317 2318 want func(*configpb.TestGroup, []InflatedColumn, map[string][]string) *statepb.Grid 2319 err bool 2320 }{ 2321 { 2322 name: "basically works", 2323 tg: &configpb.TestGroup{}, 2324 want: func(*configpb.TestGroup, []InflatedColumn, map[string][]string) *statepb.Grid { 2325 return &statepb.Grid{} 2326 }, 2327 }, 2328 { 2329 name: "unchanged", 2330 tg: &configpb.TestGroup{}, 2331 cols: []InflatedColumn{ 2332 { 2333 Column: &statepb.Column{ 2334 Name: "hi", 2335 Build: "there", 2336 }, 2337 Cells: map[string]Cell{ 2338 "cell": { 2339 Result: statuspb.TestStatus_FAIL, 2340 Message: "yo", 2341 }, 2342 }, 2343 }, 2344 { 2345 Column: &statepb.Column{ 2346 Name: "two-name", 2347 Build: "two-build", 2348 }, 2349 Cells: map[string]Cell{ 2350 "cell": { 2351 Result: statuspb.TestStatus_FAIL, 2352 Message: "yo", 2353 }, 2354 "two": { 2355 Result: statuspb.TestStatus_PASS, 2356 Icon: "S", 2357 }, 2358 }, 2359 }, 2360 }, 2361 want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid { 2362 return constructGridFromGroupConfig(logrus.New(), tg, cols, issues) 2363 }, 2364 }, 2365 { 2366 name: "truncate column data", 2367 tg: &configpb.TestGroup{}, 2368 cols: []InflatedColumn{ 2369 { 2370 Column: &statepb.Column{ 2371 Name: "hi", 2372 Build: "there", 2373 }, 2374 Cells: map[string]Cell{ 2375 "cell": { 2376 Result: statuspb.TestStatus_FAIL, 2377 Message: "yo", 2378 }, 2379 }, 2380 }, 2381 { 2382 Column: &statepb.Column{ 2383 Name: "two-name", 2384 Build: "two-build", 2385 }, 2386 Cells: func() map[string]Cell { 2387 cells := map[string]Cell{} 2388 2389 for i := 0; i < 1000; i++ { 2390 cells[fmt.Sprintf("cell-%d", i)] = Cell{ 2391 Result: statuspb.TestStatus_FAIL, 2392 Message: "yo", 2393 } 2394 } 2395 return cells 2396 }(), 2397 }, 2398 { 2399 Column: &statepb.Column{ 2400 Name: "three-name", 2401 Build: "three-build", 2402 }, 2403 Cells: map[string]Cell{ 2404 "cell": { 2405 Result: statuspb.TestStatus_FAIL, 2406 Message: "yo", 2407 }, 2408 }, 2409 }, 2410 }, 2411 ceiling: 200, 2412 want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid { 2413 expect := []InflatedColumn{ 2414 { 2415 Column: &statepb.Column{ 2416 Name: "hi", 2417 Build: "there", 2418 }, 2419 Cells: map[string]Cell{ 2420 "cell": { 2421 Result: statuspb.TestStatus_FAIL, 2422 Message: "yo", 2423 }, 2424 }, 2425 }, 2426 } 2427 logger := logrus.New() 2428 grid := constructGridFromGroupConfig(logger, tg, cols, issues) 2429 buf, _ := gcs.MarshalGrid(grid) 2430 orig := len(buf) 2431 2432 truncateLastColumn(expect, orig, 200, "byte") 2433 2434 return constructGridFromGroupConfig(logger, tg, expect, issues) 2435 }, 2436 }, 2437 { 2438 name: "truncate sparse column data", 2439 tg: &configpb.TestGroup{}, 2440 cols: []InflatedColumn{ 2441 { 2442 Column: &statepb.Column{ 2443 Name: "one", 2444 Build: "one", 2445 }, 2446 Cells: map[string]Cell{ 2447 "row-0": { 2448 Result: statuspb.TestStatus_FAIL, 2449 }, 2450 "row-1": { 2451 Result: statuspb.TestStatus_NO_RESULT, 2452 }, 2453 "row-2": { 2454 Result: statuspb.TestStatus_NO_RESULT, 2455 }, 2456 }, 2457 }, 2458 { 2459 Column: &statepb.Column{ 2460 Name: "two", 2461 Build: "two", 2462 }, 2463 Cells: func() map[string]Cell { 2464 cells := map[string]Cell{} 2465 2466 for i := 0; i < 100; i++ { 2467 cells[fmt.Sprintf("row-%d", i)] = Cell{ 2468 Result: statuspb.TestStatus_FAIL, 2469 } 2470 } 2471 return cells 2472 }(), 2473 }, 2474 { 2475 Column: &statepb.Column{ 2476 Name: "three", 2477 Build: "three", 2478 }, 2479 Cells: map[string]Cell{ 2480 "row-0": { 2481 Result: statuspb.TestStatus_FAIL, 2482 }, 2483 "row-1": { 2484 Result: statuspb.TestStatus_FAIL, 2485 }, 2486 "row-2": { 2487 Result: statuspb.TestStatus_FAIL, 2488 }, 2489 }, 2490 }, 2491 }, 2492 ceiling: 200, 2493 want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid { 2494 expect := []InflatedColumn{ 2495 // Drop the rows that are empty after trimming (e.g. row-1 and row-2). 2496 { 2497 Column: &statepb.Column{ 2498 Name: "one", 2499 Build: "one", 2500 }, 2501 Cells: map[string]Cell{ 2502 "row-0": { 2503 Result: statuspb.TestStatus_FAIL, 2504 }, 2505 }, 2506 }, 2507 } 2508 logger := logrus.New() 2509 grid := constructGridFromGroupConfig(logger, tg, cols, issues) 2510 buf, _ := gcs.MarshalGrid(grid) 2511 orig := len(buf) 2512 2513 truncateLastColumn(expect, orig, 200, "byte") 2514 2515 return constructGridFromGroupConfig(logger, tg, expect, issues) 2516 }, 2517 }, 2518 { 2519 name: "delete most column data", 2520 tg: &configpb.TestGroup{}, 2521 cols: []InflatedColumn{ 2522 { 2523 Column: &statepb.Column{ 2524 Name: "hi", 2525 Build: "there", 2526 }, 2527 Cells: map[string]Cell{ 2528 "cell": { 2529 Result: statuspb.TestStatus_FAIL, 2530 Message: "yo", 2531 }, 2532 }, 2533 }, 2534 }, 2535 ceiling: 1, // Too small for even one cell 2536 want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid { 2537 expect := []InflatedColumn{ 2538 { 2539 Column: &statepb.Column{ 2540 Name: "hi", 2541 Build: "there", 2542 }, 2543 Cells: map[string]Cell{ 2544 "Truncated": { 2545 Result: statuspb.TestStatus_UNKNOWN, 2546 Message: "The grid is too large to update. Split this testgroup into multiple testgroups.", 2547 }, 2548 }, 2549 }, 2550 } 2551 return constructGridFromGroupConfig(logrus.New(), tg, expect, nil) 2552 }, 2553 }, 2554 { 2555 name: "cancelled context", 2556 ctx: func() context.Context { 2557 ctx, cancel := context.WithCancel(context.Background()) 2558 cancel() 2559 ctx.Err() 2560 return ctx 2561 }(), 2562 tg: &configpb.TestGroup{}, 2563 cols: []InflatedColumn{ 2564 { 2565 Column: &statepb.Column{ 2566 Name: "hi", 2567 Build: "there", 2568 }, 2569 Cells: map[string]Cell{ 2570 "cell": { 2571 Result: statuspb.TestStatus_FAIL, 2572 Message: "yo", 2573 }, 2574 }, 2575 }, 2576 { 2577 Column: &statepb.Column{ 2578 Name: "two-name", 2579 Build: "two-build", 2580 }, 2581 Cells: func() map[string]Cell { 2582 cells := map[string]Cell{} 2583 2584 for i := 0; i < 1000; i++ { 2585 cells[fmt.Sprintf("cell-%d", i)] = Cell{ 2586 Result: statuspb.TestStatus_FAIL, 2587 Message: "yo", 2588 } 2589 } 2590 return cells 2591 }(), 2592 }, 2593 { 2594 Column: &statepb.Column{ 2595 Name: "three-name", 2596 Build: "three-build", 2597 }, 2598 Cells: map[string]Cell{ 2599 "cell": { 2600 Result: statuspb.TestStatus_FAIL, 2601 Message: "yo", 2602 }, 2603 }, 2604 }, 2605 }, 2606 ceiling: 100, 2607 want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid { 2608 logger := logrus.New() 2609 return constructGridFromGroupConfig(logger, tg, cols, issues) 2610 }, 2611 }, 2612 { 2613 name: "no ceiling", 2614 tg: &configpb.TestGroup{}, 2615 cols: []InflatedColumn{ 2616 { 2617 Column: &statepb.Column{ 2618 Name: "hi", 2619 Build: "there", 2620 }, 2621 Cells: map[string]Cell{ 2622 "cell": { 2623 Result: statuspb.TestStatus_FAIL, 2624 Message: "yo", 2625 }, 2626 }, 2627 }, 2628 { 2629 Column: &statepb.Column{ 2630 Name: "two-name", 2631 Build: "two-build", 2632 }, 2633 Cells: func() map[string]Cell { 2634 cells := map[string]Cell{} 2635 2636 for i := 0; i < 1000; i++ { 2637 cells[fmt.Sprintf("cell-%d", i)] = Cell{ 2638 Result: statuspb.TestStatus_FAIL, 2639 Message: "yo", 2640 } 2641 } 2642 return cells 2643 }(), 2644 }, 2645 { 2646 Column: &statepb.Column{ 2647 Name: "three-name", 2648 Build: "three-build", 2649 }, 2650 Cells: map[string]Cell{ 2651 "cell": { 2652 Result: statuspb.TestStatus_FAIL, 2653 Message: "yo", 2654 }, 2655 }, 2656 }, 2657 }, 2658 want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid { 2659 logger := logrus.New() 2660 return constructGridFromGroupConfig(logger, tg, cols, issues) 2661 }, 2662 }, 2663 } 2664 2665 for _, tc := range cases { 2666 t.Run(tc.name, func(t *testing.T) { 2667 if tc.ctx == nil { 2668 tc.ctx = context.Background() 2669 } 2670 want := tc.want(tc.tg, tc.cols, tc.issues) 2671 got, buf, err := shrinkGridInline(tc.ctx, logrus.WithField("name", tc.name), tc.tg, tc.cols, tc.issues, tc.ceiling) 2672 switch { 2673 case err != nil: 2674 if !tc.err { 2675 t.Errorf("unexpected error: %v", err) 2676 } 2677 case tc.err: 2678 t.Errorf("failed to get an error, got %v", got) 2679 default: 2680 if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { 2681 t.Errorf("unexpected grid diff (-want +got):\n%s", diff) 2682 return 2683 } 2684 wantBuf, err := gcs.MarshalGrid(want) 2685 if err != nil { 2686 t.Fatalf("Failed to marshal grid: %v", err) 2687 } 2688 if diff := cmp.Diff(wantBuf, buf); diff != "" { 2689 t.Errorf("unexpected buf diff (-want +got):\n%s", diff) 2690 } 2691 } 2692 }) 2693 } 2694 } 2695 2696 func TestOverrideBuild(t *testing.T) { 2697 cases := []struct { 2698 name string 2699 tg *configpb.TestGroup 2700 cols []InflatedColumn 2701 want []InflatedColumn 2702 }{ 2703 { 2704 name: "basically works", 2705 tg: &configpb.TestGroup{}, 2706 }, 2707 { 2708 name: "empty override does not override", 2709 tg: &configpb.TestGroup{}, 2710 cols: []InflatedColumn{ 2711 { 2712 Column: &statepb.Column{ 2713 Build: "hello", 2714 Started: 7, 2715 }, 2716 Cells: map[string]Cell{ 2717 "keep": {ID: "me"}, 2718 }, 2719 }, 2720 { 2721 Column: &statepb.Column{ 2722 Build: "world", 2723 Started: 6, 2724 }, 2725 }, 2726 }, 2727 want: []InflatedColumn{ 2728 { 2729 Column: &statepb.Column{ 2730 Build: "hello", 2731 Started: 7, 2732 }, 2733 Cells: map[string]Cell{ 2734 "keep": {ID: "me"}, 2735 }, 2736 }, 2737 { 2738 Column: &statepb.Column{ 2739 Build: "world", 2740 Started: 6, 2741 }, 2742 }, 2743 }, 2744 }, 2745 { 2746 name: "override with python style", 2747 tg: &configpb.TestGroup{ 2748 BuildOverrideStrftime: "%y-%m-%d (%Y) %H:%M:%S %p", 2749 }, 2750 cols: []InflatedColumn{ 2751 { 2752 Column: &statepb.Column{ 2753 Build: "drop", 2754 Hint: "of fruit", 2755 Name: "keep", 2756 Started: float64(time.Date(2021, 04, 22, 13, 14, 15, 0, time.Local).Unix() * 1000), 2757 }, 2758 Cells: map[string]Cell{ 2759 "keep": {ID: "me too"}, 2760 }, 2761 }, 2762 }, 2763 want: []InflatedColumn{ 2764 { 2765 Column: &statepb.Column{ 2766 Build: "21-04-22 (2021) 13:14:15 PM", 2767 Hint: "of fruit", 2768 Name: "keep", 2769 Started: float64(time.Date(2021, 04, 22, 13, 14, 15, 0, time.Local).Unix() * 1000), 2770 }, 2771 Cells: map[string]Cell{ 2772 "keep": {ID: "me too"}, 2773 }, 2774 }, 2775 }, 2776 }, 2777 { 2778 name: "override with golang format", 2779 tg: &configpb.TestGroup{ 2780 BuildOverrideStrftime: "hello 2006 PM", 2781 }, 2782 cols: []InflatedColumn{ 2783 { 2784 Column: &statepb.Column{ 2785 Build: "drop", 2786 Hint: "of fruit", 2787 Name: "keep", 2788 Started: float64(time.Date(2021, 04, 22, 13, 14, 15, 0, time.Local).Unix() * 1000), 2789 }, 2790 Cells: map[string]Cell{ 2791 "keep": {ID: "me too"}, 2792 }, 2793 }, 2794 }, 2795 want: []InflatedColumn{ 2796 { 2797 Column: &statepb.Column{ 2798 Build: "hello 2021 PM", 2799 Hint: "of fruit", 2800 Name: "keep", 2801 Started: float64(time.Date(2021, 04, 22, 13, 14, 15, 0, time.Local).Unix() * 1000), 2802 }, 2803 Cells: map[string]Cell{ 2804 "keep": {ID: "me too"}, 2805 }, 2806 }, 2807 }, 2808 }, 2809 } 2810 2811 for _, tc := range cases { 2812 t.Run(tc.name, func(t *testing.T) { 2813 overrideBuild(tc.tg, tc.cols) 2814 if diff := cmp.Diff(tc.want, tc.cols, protocmp.Transform()); diff != "" { 2815 t.Errorf("overrideBuild() got unexpected diff (-want +got):\n%s", diff) 2816 } 2817 }) 2818 } 2819 } 2820 2821 func TestGroupColumns(t *testing.T) { 2822 cases := []struct { 2823 name string 2824 tg *configpb.TestGroup 2825 cols []InflatedColumn 2826 want []InflatedColumn 2827 }{ 2828 { 2829 name: "basically works", 2830 }, 2831 { 2832 name: "single column groups do not change", 2833 cols: []InflatedColumn{ 2834 { 2835 Column: &statepb.Column{ 2836 Build: "hello", 2837 Name: "world", 2838 Started: 7, 2839 }, 2840 Cells: map[string]Cell{ 2841 "keep": {ID: "me"}, 2842 }, 2843 }, 2844 { 2845 Column: &statepb.Column{ 2846 Build: "another", 2847 Name: "column", 2848 Started: 9, 2849 }, 2850 Cells: map[string]Cell{ 2851 "also": {ID: "remains"}, 2852 }, 2853 }, 2854 }, 2855 want: []InflatedColumn{ 2856 { 2857 Column: &statepb.Column{ 2858 Build: "hello", 2859 Name: "world", 2860 Started: 7, 2861 }, 2862 Cells: map[string]Cell{ 2863 "keep": {ID: "me"}, 2864 }, 2865 }, 2866 { 2867 Column: &statepb.Column{ 2868 Build: "another", 2869 Name: "column", 2870 Started: 9, 2871 }, 2872 Cells: map[string]Cell{ 2873 "also": {ID: "remains"}, 2874 }, 2875 }, 2876 }, 2877 }, 2878 { 2879 name: "group columns with the same build and name", 2880 cols: []InflatedColumn{ 2881 { 2882 Column: &statepb.Column{ 2883 Build: "same", 2884 Name: "lemming", 2885 Hint: "99", 2886 Started: 7, 2887 Extra: []string{ 2888 "first", 2889 "", 2890 "same", 2891 "different", 2892 }, 2893 }, 2894 Cells: map[string]Cell{ 2895 "keep": {ID: "me"}, 2896 }, 2897 }, 2898 { 2899 Column: &statepb.Column{ 2900 Build: "same", 2901 Name: "lemming", 2902 Hint: "100", 2903 Started: 9, 2904 Extra: []string{ 2905 "", 2906 "second", 2907 "same", 2908 "changed", 2909 }, 2910 }, 2911 Cells: map[string]Cell{ 2912 "also": {ID: "remains"}, 2913 }, 2914 }, 2915 }, 2916 want: []InflatedColumn{ 2917 { 2918 Column: &statepb.Column{ 2919 Build: "same", 2920 Name: "lemming", 2921 Started: 7, 2922 Hint: "100", 2923 Extra: []string{ 2924 "first", 2925 "second", 2926 "same", 2927 "*", 2928 }, 2929 }, 2930 Cells: map[string]Cell{ 2931 "keep": {ID: "me"}, 2932 "also": {ID: "remains"}, 2933 }, 2934 }, 2935 }, 2936 }, 2937 { 2938 name: "group columns with the same build and name, listing all values", 2939 tg: &configpb.TestGroup{ 2940 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 2941 { 2942 Property: "", 2943 ListAllValues: true, 2944 }, 2945 { 2946 Property: "", 2947 ListAllValues: true, 2948 }, 2949 { 2950 Property: "", 2951 ListAllValues: true, 2952 }, 2953 { 2954 Property: "", 2955 ListAllValues: true, 2956 }, 2957 { 2958 Property: "", 2959 ListAllValues: true, 2960 }, 2961 }, 2962 }, 2963 cols: []InflatedColumn{ 2964 { 2965 Column: &statepb.Column{ 2966 Build: "same", 2967 Name: "lemming", 2968 Hint: "99", 2969 Started: 7, 2970 Extra: []string{ 2971 "first", 2972 "", 2973 "same", 2974 "different", 2975 "overlap||some", 2976 }, 2977 }, 2978 Cells: map[string]Cell{ 2979 "keep": {ID: "me"}, 2980 }, 2981 }, 2982 { 2983 Column: &statepb.Column{ 2984 Build: "same", 2985 Name: "lemming", 2986 Hint: "100", 2987 Started: 9, 2988 Extra: []string{ 2989 "", 2990 "second", 2991 "same", 2992 "changed", 2993 "other||overlap", 2994 }, 2995 }, 2996 Cells: map[string]Cell{ 2997 "also": {ID: "remains"}, 2998 }, 2999 }, 3000 }, 3001 want: []InflatedColumn{ 3002 { 3003 Column: &statepb.Column{ 3004 Build: "same", 3005 Name: "lemming", 3006 Started: 7, 3007 Hint: "100", 3008 Extra: []string{ 3009 "first", 3010 "second", 3011 "same", 3012 "changed||different", 3013 "other||overlap||some", 3014 }, 3015 }, 3016 Cells: map[string]Cell{ 3017 "keep": {ID: "me"}, 3018 "also": {ID: "remains"}, 3019 }, 3020 }, 3021 }, 3022 }, 3023 { 3024 name: "columns add more headers", 3025 tg: &configpb.TestGroup{ 3026 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 3027 { 3028 Property: "", 3029 }, 3030 { 3031 Property: "", 3032 }, 3033 { 3034 Property: "", 3035 }, 3036 }, 3037 }, 3038 cols: []InflatedColumn{ 3039 { 3040 Column: &statepb.Column{ 3041 Build: "same", 3042 Name: "lemming", 3043 Hint: "99", 3044 Started: 7, 3045 Extra: []string{ 3046 "one", 3047 }, 3048 }, 3049 }, 3050 { 3051 Column: &statepb.Column{ 3052 Build: "same", 3053 Name: "lemming", 3054 Hint: "100", 3055 Started: 9, 3056 Extra: []string{ 3057 "one", 3058 "two", 3059 }, 3060 }, 3061 }, 3062 { 3063 Column: &statepb.Column{ 3064 Build: "same", 3065 Name: "lemming", 3066 Hint: "100", 3067 Started: 9, 3068 Extra: []string{ 3069 "one", 3070 "two", 3071 "three", 3072 }, 3073 }, 3074 }, 3075 }, 3076 want: []InflatedColumn{ 3077 { 3078 Column: &statepb.Column{ 3079 Build: "same", 3080 Name: "lemming", 3081 Started: 7, 3082 Hint: "100", 3083 Extra: []string{ 3084 "one", 3085 "two", 3086 "three", 3087 }, 3088 }, 3089 Cells: map[string]Cell{}, 3090 }, 3091 }, 3092 }, 3093 { 3094 name: "columns remove headers", 3095 tg: &configpb.TestGroup{ 3096 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 3097 { 3098 Property: "", 3099 }, 3100 { 3101 Property: "", 3102 }, 3103 { 3104 Property: "", 3105 }, 3106 }, 3107 }, 3108 cols: []InflatedColumn{ 3109 { 3110 Column: &statepb.Column{ 3111 Build: "same", 3112 Name: "lemming", 3113 Hint: "99", 3114 Started: 7, 3115 Extra: []string{ 3116 "one", 3117 "two", 3118 "three", 3119 }, 3120 }, 3121 }, 3122 { 3123 Column: &statepb.Column{ 3124 Build: "same", 3125 Name: "lemming", 3126 Hint: "100", 3127 Started: 9, 3128 Extra: []string{ 3129 "one", 3130 "two", 3131 }, 3132 }, 3133 }, 3134 { 3135 Column: &statepb.Column{ 3136 Build: "same", 3137 Name: "lemming", 3138 Hint: "100", 3139 Started: 9, 3140 Extra: []string{ 3141 "one", 3142 }, 3143 }, 3144 }, 3145 }, 3146 want: []InflatedColumn{ 3147 { 3148 Column: &statepb.Column{ 3149 Build: "same", 3150 Name: "lemming", 3151 Started: 7, 3152 Hint: "100", 3153 Extra: []string{ 3154 "one", 3155 "two", 3156 "three", 3157 }, 3158 }, 3159 Cells: map[string]Cell{}, 3160 }, 3161 }, 3162 }, 3163 { 3164 name: "do not group different builds", 3165 cols: []InflatedColumn{ 3166 { 3167 Column: &statepb.Column{ 3168 Build: "this", 3169 Name: "same", 3170 Started: 7, 3171 }, 3172 Cells: map[string]Cell{ 3173 "keep": {ID: "me"}, 3174 }, 3175 }, 3176 { 3177 Column: &statepb.Column{ 3178 Build: "that", 3179 Name: "same", 3180 Started: 9, 3181 }, 3182 Cells: map[string]Cell{ 3183 "also": {ID: "remains"}, 3184 }, 3185 }, 3186 }, 3187 want: []InflatedColumn{ 3188 { 3189 Column: &statepb.Column{ 3190 Build: "this", 3191 Name: "same", 3192 Started: 7, 3193 }, 3194 Cells: map[string]Cell{ 3195 "keep": {ID: "me"}, 3196 }, 3197 }, 3198 { 3199 Column: &statepb.Column{ 3200 Build: "that", 3201 Name: "same", 3202 Started: 9, 3203 }, 3204 Cells: map[string]Cell{ 3205 "also": {ID: "remains"}, 3206 }, 3207 }, 3208 }, 3209 }, 3210 { 3211 name: "do not group different names", 3212 cols: []InflatedColumn{ 3213 { 3214 Column: &statepb.Column{ 3215 Build: "same", 3216 Name: "different", 3217 Started: 7, 3218 }, 3219 Cells: map[string]Cell{ 3220 "keep": {ID: "me"}, 3221 }, 3222 }, 3223 { 3224 Column: &statepb.Column{ 3225 Build: "same", 3226 Name: "changed", 3227 Started: 9, 3228 }, 3229 Cells: map[string]Cell{ 3230 "also": {ID: "remains"}, 3231 }, 3232 }, 3233 }, 3234 want: []InflatedColumn{ 3235 { 3236 Column: &statepb.Column{ 3237 Build: "same", 3238 Name: "different", 3239 Started: 7, 3240 }, 3241 Cells: map[string]Cell{ 3242 "keep": {ID: "me"}, 3243 }, 3244 }, 3245 { 3246 Column: &statepb.Column{ 3247 Build: "same", 3248 Name: "changed", 3249 Started: 9, 3250 }, 3251 Cells: map[string]Cell{ 3252 "also": {ID: "remains"}, 3253 }, 3254 }, 3255 }, 3256 }, 3257 { 3258 name: "split merged rows with the same name", 3259 cols: []InflatedColumn{ 3260 { 3261 Column: &statepb.Column{ 3262 Build: "same", 3263 Name: "same", 3264 Started: 7, 3265 }, 3266 Cells: map[string]Cell{ 3267 "first": {ID: "first"}, 3268 "same": {ID: "first-different"}, 3269 }, 3270 }, 3271 { 3272 Column: &statepb.Column{ 3273 Build: "same", 3274 Name: "same", 3275 Started: 9, 3276 }, 3277 Cells: map[string]Cell{ 3278 "same": {ID: "second-changed"}, 3279 "second": {ID: "second"}, 3280 }, 3281 }, 3282 }, 3283 want: []InflatedColumn{ 3284 { 3285 Column: &statepb.Column{ 3286 Build: "same", 3287 Name: "same", 3288 Started: 7, 3289 }, 3290 Cells: map[string]Cell{ 3291 "first": {ID: "first"}, 3292 "same": {ID: "first-different"}, 3293 "same [1]": {ID: "second-changed"}, 3294 "second": {ID: "second"}, 3295 }, 3296 }, 3297 }, 3298 }, 3299 { 3300 name: "ignore_old_results only takes newest", 3301 tg: &configpb.TestGroup{ 3302 IgnoreOldResults: true, 3303 }, 3304 cols: []InflatedColumn{ 3305 { 3306 Column: &statepb.Column{ 3307 Build: "same", 3308 Name: "same", 3309 Started: 9, 3310 }, 3311 Cells: map[string]Cell{ 3312 "first": {ID: "first"}, 3313 "same": {ID: "first-different"}, 3314 }, 3315 }, 3316 { 3317 Column: &statepb.Column{ 3318 Build: "same", 3319 Name: "same", 3320 Started: 7, 3321 }, 3322 Cells: map[string]Cell{ 3323 "same": {ID: "second-changed"}, 3324 "second": {ID: "second"}, 3325 }, 3326 }, 3327 }, 3328 want: []InflatedColumn{ 3329 { 3330 Column: &statepb.Column{ 3331 Build: "same", 3332 Name: "same", 3333 Started: 7, 3334 }, 3335 Cells: map[string]Cell{ 3336 "first": {ID: "first"}, 3337 "same": {ID: "first-different"}, 3338 "second": {ID: "second"}, 3339 }, 3340 }, 3341 }, 3342 }, 3343 } 3344 3345 for _, tc := range cases { 3346 t.Run(tc.name, func(t *testing.T) { 3347 tg := tc.tg 3348 if tg == nil { 3349 tg = &configpb.TestGroup{} 3350 } 3351 got := groupColumns(tg, tc.cols) 3352 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 3353 t.Errorf("groupColumns() got unexpected diff (-want +got):\n%s", diff) 3354 } 3355 }) 3356 } 3357 } 3358 3359 // TODO(amerai): This is kind of, but not quite, redundant with summary.gridMetrics(). 3360 func TestColumnStats(t *testing.T) { 3361 passCell := Cell{Result: statuspb.TestStatus_PASS} 3362 failCell := Cell{Result: statuspb.TestStatus_FAIL} 3363 3364 cases := []struct { 3365 name string 3366 cells map[string]Cell 3367 brokenThreshold float32 3368 want *statepb.Stats 3369 }{ 3370 { 3371 name: "nil", 3372 brokenThreshold: 0.5, 3373 want: nil, 3374 }, 3375 { 3376 name: "empty", 3377 brokenThreshold: 0.5, 3378 cells: map[string]Cell{}, 3379 want: &statepb.Stats{}, 3380 }, 3381 { 3382 name: "nil, no threshold", 3383 want: nil, 3384 }, 3385 { 3386 name: "empty, no threshold", 3387 cells: map[string]Cell{}, 3388 want: nil, 3389 }, 3390 { 3391 name: "no threshold", 3392 cells: map[string]Cell{ 3393 "a": passCell, 3394 "b": failCell, 3395 }, 3396 want: nil, 3397 }, 3398 { 3399 name: "blank", 3400 brokenThreshold: 0.5, 3401 cells: map[string]Cell{ 3402 "a": emptyCell, 3403 "b": emptyCell, 3404 "c": emptyCell, 3405 "d": emptyCell, 3406 }, 3407 want: &statepb.Stats{ 3408 PassCount: 0, 3409 FailCount: 0, 3410 TotalCount: 0, 3411 }, 3412 }, 3413 { 3414 name: "passing", 3415 brokenThreshold: 0.5, 3416 cells: map[string]Cell{ 3417 "a": passCell, 3418 "b": passCell, 3419 "c": passCell, 3420 "d": passCell, 3421 }, 3422 want: &statepb.Stats{ 3423 PassCount: 4, 3424 FailCount: 0, 3425 TotalCount: 4, 3426 }, 3427 }, 3428 { 3429 name: "failing", 3430 brokenThreshold: 0.5, 3431 cells: map[string]Cell{ 3432 "a": failCell, 3433 "b": failCell, 3434 "c": failCell, 3435 "d": failCell, 3436 }, 3437 want: &statepb.Stats{ 3438 PassCount: 0, 3439 FailCount: 4, 3440 TotalCount: 4, 3441 Broken: true, 3442 }, 3443 }, 3444 { 3445 name: "mix, not broken", 3446 brokenThreshold: 0.5, 3447 cells: map[string]Cell{ 3448 "a": passCell, 3449 "b": passCell, 3450 "c": failCell, 3451 "d": passCell, 3452 }, 3453 want: &statepb.Stats{ 3454 PassCount: 3, 3455 FailCount: 1, 3456 TotalCount: 4, 3457 }, 3458 }, 3459 { 3460 name: "mix, broken", 3461 brokenThreshold: 0.5, 3462 cells: map[string]Cell{ 3463 "a": failCell, 3464 "b": passCell, 3465 "c": failCell, 3466 "d": failCell, 3467 }, 3468 want: &statepb.Stats{ 3469 PassCount: 1, 3470 FailCount: 3, 3471 TotalCount: 4, 3472 Broken: true, 3473 }, 3474 }, 3475 { 3476 name: "mix, blank cells", 3477 brokenThreshold: 0.5, 3478 cells: map[string]Cell{ 3479 "a": failCell, 3480 "b": passCell, 3481 "c": emptyCell, 3482 "d": emptyCell, 3483 "e": failCell, 3484 "f": passCell, 3485 "g": failCell, 3486 "h": emptyCell, 3487 }, 3488 want: &statepb.Stats{ 3489 PassCount: 2, 3490 FailCount: 3, 3491 TotalCount: 5, 3492 Broken: true, 3493 }, 3494 }, 3495 { 3496 name: "pending", 3497 brokenThreshold: 0.5, 3498 cells: map[string]Cell{ 3499 "a": failCell, 3500 "b": passCell, 3501 "c": passCell, 3502 "d": {Result: statuspb.TestStatus_RUNNING}, 3503 }, 3504 want: &statepb.Stats{ 3505 PassCount: 2, 3506 FailCount: 1, 3507 TotalCount: 4, 3508 Pending: true, 3509 }, 3510 }, 3511 { 3512 name: "advanced", 3513 brokenThreshold: 0.5, 3514 cells: map[string]Cell{ 3515 "a": failCell, 3516 "b": passCell, 3517 "c": emptyCell, 3518 "d": {Result: statuspb.TestStatus_BLOCKED}, 3519 "e": {Result: statuspb.TestStatus_BUILD_FAIL}, 3520 "f": {Result: statuspb.TestStatus_BUILD_PASSED}, 3521 "g": {Result: statuspb.TestStatus_CANCEL}, 3522 "h": {Result: statuspb.TestStatus_CATEGORIZED_ABORT}, 3523 "i": {Result: statuspb.TestStatus_CATEGORIZED_FAIL}, 3524 "j": {Result: statuspb.TestStatus_FLAKY}, 3525 "k": {Result: statuspb.TestStatus_PASS_WITH_ERRORS}, 3526 "l": {Result: statuspb.TestStatus_PASS_WITH_SKIPS}, 3527 "m": {Result: statuspb.TestStatus_TIMED_OUT}, 3528 "n": {Result: statuspb.TestStatus_TOOL_FAIL}, 3529 "o": {Result: statuspb.TestStatus_UNKNOWN}, 3530 "p": {Result: statuspb.TestStatus_RUNNING}, 3531 }, 3532 want: &statepb.Stats{ 3533 PassCount: 4, 3534 FailCount: 5, 3535 TotalCount: 15, 3536 Pending: true, 3537 }, 3538 }, 3539 } 3540 3541 for _, tc := range cases { 3542 t.Run(tc.name, func(t *testing.T) { 3543 got := columnStats(tc.cells, tc.brokenThreshold) 3544 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 3545 t.Errorf("columnStats(%v, %f) got unexpected diff (-want +got):\n%s", tc.cells, tc.brokenThreshold, diff) 3546 } 3547 }) 3548 } 3549 } 3550 3551 func TestConstructGrid(t *testing.T) { 3552 defaultCustomColumnHeaders := []*configpb.TestGroup_ColumnHeader{ 3553 { 3554 Property: "hello", 3555 ConfigurationValue: "world!", 3556 }, 3557 { 3558 Property: "foo", 3559 ConfigurationValue: "bar", 3560 }, 3561 } 3562 cases := []struct { 3563 name string 3564 cols []inflatedColumn 3565 numFailuresToAlert int 3566 numPassesToDisableAlert int 3567 issues map[string][]string 3568 brokenThreshold float32 3569 columnHeader []*configpb.TestGroup_ColumnHeader 3570 expected *statepb.Grid 3571 }{ 3572 { 3573 name: "basically works", 3574 expected: &statepb.Grid{}, 3575 }, 3576 { 3577 name: "multiple columns", 3578 columnHeader: defaultCustomColumnHeaders, 3579 cols: []inflatedColumn{ 3580 { 3581 Column: &statepb.Column{Build: "15"}, 3582 Cells: map[string]cell{ 3583 "green": { 3584 Result: statuspb.TestStatus_PASS, 3585 }, 3586 "red": { 3587 Result: statuspb.TestStatus_FAIL, 3588 }, 3589 "only-15": { 3590 Result: statuspb.TestStatus_FLAKY, 3591 }, 3592 }, 3593 }, 3594 { 3595 Column: &statepb.Column{Build: "10"}, 3596 Cells: map[string]cell{ 3597 "full": { 3598 Result: statuspb.TestStatus_PASS, 3599 CellID: "cell", 3600 Icon: "icon", 3601 Message: "message", 3602 Metrics: map[string]float64{ 3603 "elapsed": 1, 3604 "keys": 2, 3605 }, 3606 UserProperty: "food", 3607 }, 3608 "green": { 3609 Result: statuspb.TestStatus_PASS, 3610 }, 3611 "red": { 3612 Result: statuspb.TestStatus_FAIL, 3613 }, 3614 "only-10": { 3615 Result: statuspb.TestStatus_FLAKY, 3616 }, 3617 }, 3618 }, 3619 }, 3620 expected: &statepb.Grid{ 3621 Columns: []*statepb.Column{ 3622 {Build: "15"}, 3623 {Build: "10"}, 3624 }, 3625 Rows: []*statepb.Row{ 3626 setupRow( 3627 &statepb.Row{ 3628 Name: "full", 3629 Id: "full", 3630 UserProperty: []string{}, 3631 }, 3632 emptyCell, 3633 cell{ 3634 Result: statuspb.TestStatus_PASS, 3635 CellID: "cell", 3636 Icon: "icon", 3637 Message: "message", 3638 Metrics: map[string]float64{ 3639 "elapsed": 1, 3640 "keys": 2, 3641 }, 3642 UserProperty: "food", 3643 }, 3644 ), 3645 setupRow( 3646 &statepb.Row{ 3647 Name: "green", 3648 Id: "green", 3649 }, 3650 cell{Result: statuspb.TestStatus_PASS}, 3651 cell{Result: statuspb.TestStatus_PASS}, 3652 ), 3653 setupRow( 3654 &statepb.Row{ 3655 Name: "only-10", 3656 Id: "only-10", 3657 }, 3658 emptyCell, 3659 cell{Result: statuspb.TestStatus_FLAKY}, 3660 ), 3661 setupRow( 3662 &statepb.Row{ 3663 Name: "only-15", 3664 Id: "only-15", 3665 }, 3666 cell{Result: statuspb.TestStatus_FLAKY}, 3667 emptyCell, 3668 ), 3669 setupRow( 3670 &statepb.Row{ 3671 Name: "red", 3672 Id: "red", 3673 }, 3674 cell{Result: statuspb.TestStatus_FAIL}, 3675 cell{Result: statuspb.TestStatus_FAIL}, 3676 ), 3677 }, 3678 }, 3679 }, 3680 { 3681 name: "multiple columns with threshold", 3682 columnHeader: defaultCustomColumnHeaders, 3683 brokenThreshold: 0.3, 3684 cols: []inflatedColumn{ 3685 { 3686 Column: &statepb.Column{Build: "15"}, 3687 Cells: map[string]cell{ 3688 "green": { 3689 Result: statuspb.TestStatus_PASS, 3690 }, 3691 "red": { 3692 Result: statuspb.TestStatus_FAIL, 3693 }, 3694 "only-15": { 3695 Result: statuspb.TestStatus_FLAKY, 3696 }, 3697 }, 3698 }, 3699 { 3700 Column: &statepb.Column{Build: "10"}, 3701 Cells: map[string]cell{ 3702 "full": { 3703 Result: statuspb.TestStatus_PASS, 3704 CellID: "cell", 3705 Icon: "icon", 3706 Message: "message", 3707 Metrics: map[string]float64{ 3708 "elapsed": 1, 3709 "keys": 2, 3710 }, 3711 UserProperty: "food", 3712 }, 3713 "green": { 3714 Result: statuspb.TestStatus_PASS, 3715 }, 3716 "red": { 3717 Result: statuspb.TestStatus_FAIL, 3718 }, 3719 "only-10": { 3720 Result: statuspb.TestStatus_FLAKY, 3721 }, 3722 }, 3723 }, 3724 }, 3725 expected: &statepb.Grid{ 3726 Columns: []*statepb.Column{ 3727 { 3728 Build: "15", 3729 Stats: &statepb.Stats{ 3730 FailCount: 1, 3731 PassCount: 1, 3732 TotalCount: 3, 3733 Broken: true, 3734 }, 3735 }, 3736 { 3737 Build: "10", 3738 Stats: &statepb.Stats{ 3739 FailCount: 1, 3740 PassCount: 2, 3741 TotalCount: 4, 3742 }, 3743 }, 3744 }, 3745 Rows: []*statepb.Row{ 3746 setupRow( 3747 &statepb.Row{ 3748 Name: "full", 3749 Id: "full", 3750 UserProperty: []string{}, 3751 }, 3752 emptyCell, 3753 cell{ 3754 Result: statuspb.TestStatus_PASS, 3755 CellID: "cell", 3756 Icon: "icon", 3757 Message: "message", 3758 Metrics: map[string]float64{ 3759 "elapsed": 1, 3760 "keys": 2, 3761 }, 3762 UserProperty: "food", 3763 }, 3764 ), 3765 setupRow( 3766 &statepb.Row{ 3767 Name: "green", 3768 Id: "green", 3769 }, 3770 cell{Result: statuspb.TestStatus_PASS}, 3771 cell{Result: statuspb.TestStatus_PASS}, 3772 ), 3773 setupRow( 3774 &statepb.Row{ 3775 Name: "only-10", 3776 Id: "only-10", 3777 }, 3778 emptyCell, 3779 cell{Result: statuspb.TestStatus_FLAKY}, 3780 ), 3781 setupRow( 3782 &statepb.Row{ 3783 Name: "only-15", 3784 Id: "only-15", 3785 }, 3786 cell{Result: statuspb.TestStatus_FLAKY}, 3787 emptyCell, 3788 ), 3789 setupRow( 3790 &statepb.Row{ 3791 Name: "red", 3792 Id: "red", 3793 }, 3794 cell{Result: statuspb.TestStatus_FAIL}, 3795 cell{Result: statuspb.TestStatus_FAIL}, 3796 ), 3797 }, 3798 }, 3799 }, 3800 { 3801 name: "open alert", 3802 numFailuresToAlert: 2, 3803 numPassesToDisableAlert: 2, 3804 cols: []inflatedColumn{ 3805 { 3806 Column: &statepb.Column{Build: "4"}, 3807 Cells: map[string]cell{ 3808 "just-flaky": { 3809 Result: statuspb.TestStatus_FAIL, 3810 }, 3811 "broken": { 3812 Result: statuspb.TestStatus_FAIL, 3813 }, 3814 }, 3815 }, 3816 { 3817 Column: &statepb.Column{Build: "3"}, 3818 Cells: map[string]cell{ 3819 "just-flaky": { 3820 Result: statuspb.TestStatus_PASS, 3821 }, 3822 "broken": { 3823 Result: statuspb.TestStatus_FAIL, 3824 }, 3825 }, 3826 }, 3827 }, 3828 expected: &statepb.Grid{ 3829 Columns: []*statepb.Column{ 3830 {Build: "4"}, 3831 {Build: "3"}, 3832 }, 3833 Rows: []*statepb.Row{ 3834 setupRow( 3835 &statepb.Row{ 3836 Name: "broken", 3837 Id: "broken", 3838 }, 3839 cell{Result: statuspb.TestStatus_FAIL}, 3840 cell{Result: statuspb.TestStatus_FAIL}, 3841 ), 3842 setupRow( 3843 &statepb.Row{ 3844 Name: "just-flaky", 3845 Id: "just-flaky", 3846 }, 3847 cell{Result: statuspb.TestStatus_FAIL}, 3848 cell{Result: statuspb.TestStatus_PASS}, 3849 ), 3850 }, 3851 }, 3852 }, 3853 { 3854 name: "close alert", 3855 numFailuresToAlert: 1, 3856 numPassesToDisableAlert: 2, 3857 cols: []inflatedColumn{ 3858 { 3859 Column: &statepb.Column{Build: "4"}, 3860 Cells: map[string]cell{ 3861 "still-broken": { 3862 Result: statuspb.TestStatus_PASS, 3863 }, 3864 "fixed": { 3865 Result: statuspb.TestStatus_PASS, 3866 }, 3867 }, 3868 }, 3869 { 3870 Column: &statepb.Column{Build: "3"}, 3871 Cells: map[string]cell{ 3872 "still-broken": { 3873 Result: statuspb.TestStatus_FAIL, 3874 }, 3875 "fixed": { 3876 Result: statuspb.TestStatus_PASS, 3877 }, 3878 }, 3879 }, 3880 { 3881 Column: &statepb.Column{Build: "2"}, 3882 Cells: map[string]cell{ 3883 "still-broken": { 3884 Result: statuspb.TestStatus_FAIL, 3885 }, 3886 "fixed": { 3887 Result: statuspb.TestStatus_FAIL, 3888 }, 3889 }, 3890 }, 3891 { 3892 Column: &statepb.Column{Build: "1"}, 3893 Cells: map[string]cell{ 3894 "still-broken": { 3895 Result: statuspb.TestStatus_FAIL, 3896 }, 3897 "fixed": { 3898 Result: statuspb.TestStatus_FAIL, 3899 }, 3900 }, 3901 }, 3902 }, 3903 expected: &statepb.Grid{ 3904 Columns: []*statepb.Column{ 3905 {Build: "4"}, 3906 {Build: "3"}, 3907 {Build: "2"}, 3908 {Build: "1"}, 3909 }, 3910 Rows: []*statepb.Row{ 3911 setupRow( 3912 &statepb.Row{ 3913 Name: "fixed", 3914 Id: "fixed", 3915 }, 3916 cell{Result: statuspb.TestStatus_PASS}, 3917 cell{Result: statuspb.TestStatus_PASS}, 3918 cell{Result: statuspb.TestStatus_FAIL}, 3919 cell{Result: statuspb.TestStatus_FAIL}, 3920 ), 3921 setupRow( 3922 &statepb.Row{ 3923 Name: "still-broken", 3924 Id: "still-broken", 3925 }, 3926 cell{Result: statuspb.TestStatus_PASS}, 3927 cell{Result: statuspb.TestStatus_FAIL}, 3928 cell{Result: statuspb.TestStatus_FAIL}, 3929 cell{Result: statuspb.TestStatus_FAIL}, 3930 ), 3931 }, 3932 }, 3933 }, 3934 { 3935 name: "issues", 3936 cols: []inflatedColumn{ 3937 { 3938 Column: &statepb.Column{Build: "15"}, 3939 Cells: map[string]cell{ 3940 "row": { 3941 Result: statuspb.TestStatus_PASS, 3942 Issues: []string{ 3943 "from-cell-15", 3944 "should-deduplicate-from-both", 3945 "should-deduplicate-from-row", 3946 "should-deduplicate-from-cell", 3947 "should-deduplicate-from-cell", 3948 }, 3949 }, 3950 }, 3951 }, 3952 { 3953 Column: &statepb.Column{Build: "10"}, 3954 Cells: map[string]cell{ 3955 "row": { 3956 Result: statuspb.TestStatus_PASS, 3957 Issues: []string{ 3958 "from-cell-10", 3959 "should-deduplicate-from-row", 3960 }, 3961 }, 3962 "other": { 3963 Result: statuspb.TestStatus_PASS, 3964 Issues: []string{"fun"}, 3965 }, 3966 "sort": { 3967 Result: statuspb.TestStatus_PASS, 3968 Issues: []string{ 3969 "3-is-second", 3970 "100-is-last", 3971 "2-is-first", 3972 }, 3973 }, 3974 }, 3975 }, 3976 }, 3977 issues: map[string][]string{ 3978 "row": { 3979 "from-argument", 3980 "should-deduplicate-from-arg", 3981 "should-deduplicate-from-arg", 3982 "should-deduplicate-from-both", 3983 }, 3984 }, 3985 expected: &statepb.Grid{ 3986 Columns: []*statepb.Column{ 3987 {Build: "15"}, 3988 {Build: "10"}, 3989 }, 3990 Rows: []*statepb.Row{ 3991 setupRow( 3992 &statepb.Row{ 3993 Name: "other", 3994 Id: "other", 3995 Issues: []string{"fun"}, 3996 }, 3997 cell{Result: statuspb.TestStatus_NO_RESULT}, 3998 cell{Result: statuspb.TestStatus_PASS}, 3999 ), 4000 setupRow( 4001 &statepb.Row{ 4002 Name: "row", 4003 Id: "row", 4004 Issues: []string{ 4005 "should-deduplicate-from-row", 4006 "should-deduplicate-from-cell", 4007 "should-deduplicate-from-both", 4008 "should-deduplicate-from-arg", 4009 "from-cell-15", 4010 "from-cell-10", 4011 "from-argument", 4012 }, 4013 }, 4014 cell{Result: statuspb.TestStatus_PASS}, 4015 cell{Result: statuspb.TestStatus_PASS}, 4016 ), 4017 setupRow( 4018 &statepb.Row{ 4019 Name: "sort", 4020 Id: "sort", 4021 Issues: []string{ 4022 "100-is-last", 4023 "3-is-second", 4024 "2-is-first", 4025 }, 4026 }, 4027 cell{Result: statuspb.TestStatus_NO_RESULT}, 4028 cell{Result: statuspb.TestStatus_PASS}, 4029 ), 4030 }, 4031 }, 4032 }, 4033 } 4034 4035 for _, tc := range cases { 4036 t.Run(tc.name, func(t *testing.T) { 4037 actual := ConstructGrid(logrus.WithField("name", tc.name), tc.cols, tc.issues, tc.numFailuresToAlert, tc.numPassesToDisableAlert, true, "userProperty", tc.brokenThreshold, tc.columnHeader) 4038 alertRows(tc.expected.Columns, tc.expected.Rows, tc.numFailuresToAlert, tc.numPassesToDisableAlert, true, "userProperty", tc.columnHeader) 4039 for _, row := range tc.expected.Rows { 4040 sort.SliceStable(row.Metric, func(i, j int) bool { 4041 return sortorder.NaturalLess(row.Metric[i], row.Metric[j]) 4042 }) 4043 sort.SliceStable(row.Metrics, func(i, j int) bool { 4044 return sortorder.NaturalLess(row.Metrics[i].Name, row.Metrics[j].Name) 4045 }) 4046 } 4047 if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" { 4048 t.Errorf("ConstructGrid() got unexpected diff (-want +got):\n%s", diff) 4049 } 4050 }) 4051 } 4052 } 4053 4054 func TestAppendMetric(t *testing.T) { 4055 cases := []struct { 4056 name string 4057 metric *statepb.Metric 4058 idx int32 4059 value float64 4060 expected *statepb.Metric 4061 }{ 4062 { 4063 name: "basically works", 4064 metric: &statepb.Metric{}, 4065 expected: &statepb.Metric{ 4066 Indices: []int32{0, 1}, 4067 Values: []float64{0}, 4068 }, 4069 }, 4070 { 4071 name: "start metric at random column", 4072 metric: &statepb.Metric{}, 4073 idx: 7, 4074 value: 11, 4075 expected: &statepb.Metric{ 4076 Indices: []int32{7, 1}, 4077 Values: []float64{11}, 4078 }, 4079 }, 4080 { 4081 name: "continue existing series", 4082 metric: &statepb.Metric{ 4083 Indices: []int32{6, 2}, 4084 Values: []float64{6.1, 6.2}, 4085 }, 4086 idx: 8, 4087 value: 88, 4088 expected: &statepb.Metric{ 4089 Indices: []int32{6, 3}, 4090 Values: []float64{6.1, 6.2, 88}, 4091 }, 4092 }, 4093 { 4094 name: "start new series", 4095 metric: &statepb.Metric{ 4096 Indices: []int32{3, 2}, 4097 Values: []float64{6.1, 6.2}, 4098 }, 4099 idx: 8, 4100 value: 88, 4101 expected: &statepb.Metric{ 4102 Indices: []int32{3, 2, 8, 1}, 4103 Values: []float64{6.1, 6.2, 88}, 4104 }, 4105 }, 4106 } 4107 4108 for _, tc := range cases { 4109 t.Run(tc.name, func(t *testing.T) { 4110 appendMetric(tc.metric, tc.idx, tc.value) 4111 if diff := cmp.Diff(tc.metric, tc.expected, protocmp.Transform()); diff != "" { 4112 t.Errorf("appendMetric() got unexpected diff (-got +want):\n%s", diff) 4113 } 4114 }) 4115 } 4116 } 4117 4118 func TestAppendCell(t *testing.T) { 4119 cases := []struct { 4120 name string 4121 row *statepb.Row 4122 cell cell 4123 start int 4124 count int 4125 4126 expected *statepb.Row 4127 }{ 4128 { 4129 name: "basically works", 4130 row: &statepb.Row{}, 4131 expected: &statepb.Row{ 4132 Results: []int32{0, 0}, 4133 }, 4134 }, 4135 { 4136 name: "first result", 4137 row: &statepb.Row{}, 4138 cell: cell{ 4139 Result: statuspb.TestStatus_PASS, 4140 }, 4141 count: 1, 4142 expected: &statepb.Row{ 4143 Results: []int32{int32(statuspb.TestStatus_PASS), 1}, 4144 CellIds: []string{""}, 4145 Messages: []string{""}, 4146 Icons: []string{""}, 4147 UserProperty: []string{""}, 4148 Properties: []*statepb.Property{{}}, 4149 }, 4150 }, 4151 { 4152 name: "all fields filled", 4153 row: &statepb.Row{}, 4154 cell: cell{ 4155 Result: statuspb.TestStatus_PASS, 4156 CellID: "cell-id", 4157 Message: "hi", 4158 Icon: "there", 4159 Metrics: map[string]float64{ 4160 "pi": 3.14, 4161 "golden": 1.618, 4162 }, 4163 UserProperty: "hello", 4164 Properties: map[string]string{ 4165 "workflow-id": "run-1", 4166 "workflow-name": "//workflow-a", 4167 }, 4168 }, 4169 count: 1, 4170 expected: &statepb.Row{ 4171 Results: []int32{int32(statuspb.TestStatus_PASS), 1}, 4172 CellIds: []string{"cell-id"}, 4173 Messages: []string{"hi"}, 4174 Icons: []string{"there"}, 4175 Metric: []string{ 4176 "golden", 4177 "pi", 4178 }, 4179 Metrics: []*statepb.Metric{ 4180 { 4181 Name: "pi", 4182 Indices: []int32{0, 1}, 4183 Values: []float64{3.14}, 4184 }, 4185 { 4186 Name: "golden", 4187 Indices: []int32{0, 1}, 4188 Values: []float64{1.618}, 4189 }, 4190 }, 4191 UserProperty: []string{"hello"}, 4192 Properties: []*statepb.Property{{ 4193 Property: map[string]string{ 4194 "workflow-id": "run-1", 4195 "workflow-name": "//workflow-a", 4196 }, 4197 }}, 4198 }, 4199 }, 4200 { 4201 name: "append same result", 4202 row: &statepb.Row{ 4203 Results: []int32{ 4204 int32(statuspb.TestStatus_FLAKY), 3, 4205 }, 4206 CellIds: []string{"", "", ""}, 4207 Messages: []string{"", "", ""}, 4208 Icons: []string{"", "", ""}, 4209 UserProperty: []string{"", "", ""}, 4210 Properties: []*statepb.Property{{}, {}, {}}, 4211 }, 4212 cell: cell{ 4213 Result: statuspb.TestStatus_FLAKY, 4214 Message: "echo", 4215 CellID: "again and", 4216 Icon: "keeps going", 4217 UserProperty: "more more", 4218 Properties: map[string]string{ 4219 "workflow-id": "run-1", 4220 "workflow-name": "//workflow-a", 4221 }, 4222 }, 4223 count: 2, 4224 expected: &statepb.Row{ 4225 Results: []int32{int32(statuspb.TestStatus_FLAKY), 5}, 4226 CellIds: []string{"", "", "", "again and", "again and"}, 4227 Messages: []string{"", "", "", "echo", "echo"}, 4228 Icons: []string{"", "", "", "keeps going", "keeps going"}, 4229 UserProperty: []string{"", "", "", "more more", "more more"}, 4230 Properties: []*statepb.Property{ 4231 {}, 4232 {}, 4233 {}, 4234 { 4235 Property: map[string]string{ 4236 "workflow-id": "run-1", 4237 "workflow-name": "//workflow-a", 4238 }, 4239 }, 4240 { 4241 Property: map[string]string{ 4242 "workflow-id": "run-1", 4243 "workflow-name": "//workflow-a", 4244 }, 4245 }, 4246 }, 4247 }, 4248 }, 4249 { 4250 name: "append different result", 4251 row: &statepb.Row{ 4252 Results: []int32{ 4253 int32(statuspb.TestStatus_FLAKY), 3, 4254 }, 4255 CellIds: []string{"", "", ""}, 4256 Messages: []string{"", "", ""}, 4257 Icons: []string{"", "", ""}, 4258 UserProperty: []string{"", "", ""}, 4259 Properties: []*statepb.Property{{}, {}, {}}, 4260 }, 4261 cell: cell{ 4262 Result: statuspb.TestStatus_PASS, 4263 }, 4264 count: 2, 4265 expected: &statepb.Row{ 4266 Results: []int32{ 4267 int32(statuspb.TestStatus_FLAKY), 3, 4268 int32(statuspb.TestStatus_PASS), 2, 4269 }, 4270 CellIds: []string{"", "", "", "", ""}, 4271 Messages: []string{"", "", "", "", ""}, 4272 Icons: []string{"", "", "", "", ""}, 4273 UserProperty: []string{"", "", "", "", ""}, 4274 Properties: []*statepb.Property{{}, {}, {}, {}, {}}, 4275 }, 4276 }, 4277 { 4278 name: "append no Result (results, no cellIDs, messages or icons)", 4279 row: &statepb.Row{ 4280 Results: []int32{ 4281 int32(statuspb.TestStatus_FLAKY), 3, 4282 }, 4283 CellIds: []string{"", "", ""}, 4284 Messages: []string{"", "", ""}, 4285 Icons: []string{"", "", ""}, 4286 UserProperty: []string{"", "", ""}, 4287 Properties: []*statepb.Property{{}, {}, {}}, 4288 }, 4289 cell: cell{ 4290 Result: statuspb.TestStatus_NO_RESULT, 4291 }, 4292 count: 2, 4293 expected: &statepb.Row{ 4294 Results: []int32{ 4295 int32(statuspb.TestStatus_FLAKY), 3, 4296 int32(statuspb.TestStatus_NO_RESULT), 2, 4297 }, 4298 CellIds: []string{"", "", ""}, 4299 Messages: []string{"", "", ""}, 4300 Icons: []string{"", "", ""}, 4301 UserProperty: []string{"", "", ""}, 4302 Properties: []*statepb.Property{{}, {}, {}}, 4303 }, 4304 }, 4305 { 4306 name: "add metric to series", 4307 row: &statepb.Row{ 4308 Results: []int32{int32(statuspb.TestStatus_PASS), 5}, 4309 CellIds: []string{"", "", "", "", "c"}, 4310 Messages: []string{"", "", "", "", "m"}, 4311 Icons: []string{"", "", "", "", "i"}, 4312 UserProperty: []string{"", "", "", "", "up"}, 4313 Properties: []*statepb.Property{{}, {}, {}, {}, {}}, 4314 Metric: []string{ 4315 "continued-series", 4316 "new-series", 4317 }, 4318 Metrics: []*statepb.Metric{ 4319 { 4320 Name: "continued-series", 4321 Indices: []int32{0, 5}, 4322 Values: []float64{0, 1, 2, 3, 4}, 4323 }, 4324 { 4325 Name: "new-series", 4326 Indices: []int32{2, 2}, 4327 Values: []float64{2, 3}, 4328 }, 4329 }, 4330 }, 4331 cell: cell{ 4332 Result: statuspb.TestStatus_PASS, 4333 Metrics: map[string]float64{ 4334 "continued-series": 5.1, 4335 "new-series": 5.2, 4336 "additional-metric": 5.3, 4337 }, 4338 }, 4339 start: 5, 4340 count: 1, 4341 expected: &statepb.Row{ 4342 Results: []int32{int32(statuspb.TestStatus_PASS), 6}, 4343 CellIds: []string{"", "", "", "", "c", ""}, 4344 Messages: []string{"", "", "", "", "m", ""}, 4345 Icons: []string{"", "", "", "", "i", ""}, 4346 UserProperty: []string{"", "", "", "", "up", ""}, 4347 Properties: []*statepb.Property{{}, {}, {}, {}, {}, {}}, 4348 Metric: []string{ 4349 "continued-series", 4350 "new-series", 4351 "additional-metric", 4352 }, 4353 Metrics: []*statepb.Metric{ 4354 { 4355 Name: "continued-series", 4356 Indices: []int32{0, 6}, 4357 Values: []float64{0, 1, 2, 3, 4, 5.1}, 4358 }, 4359 { 4360 Name: "new-series", 4361 Indices: []int32{2, 2, 5, 1}, 4362 Values: []float64{2, 3, 5.2}, 4363 }, 4364 { 4365 Name: "additional-metric", 4366 Indices: []int32{5, 1}, 4367 Values: []float64{5.3}, 4368 }, 4369 }, 4370 }, 4371 }, 4372 { 4373 name: "add a bunch of initial blank columns (eg a deleted row)", 4374 row: &statepb.Row{}, 4375 cell: emptyCell, 4376 count: 7, 4377 expected: &statepb.Row{ 4378 Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 7}, 4379 }, 4380 }, 4381 { 4382 name: "issues", 4383 row: &statepb.Row{}, 4384 count: 395, 4385 cell: Cell{ 4386 Issues: []string{"problematic", "state"}, 4387 }, 4388 expected: &statepb.Row{ 4389 Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 395}, 4390 Issues: []string{"problematic", "state"}, 4391 }, 4392 }, 4393 { 4394 name: "append to group delimiter", 4395 row: &statepb.Row{ 4396 Name: "test1@TESTGRID@something", 4397 Results: []int32{ 4398 int32(statuspb.TestStatus_PASS), 1, 4399 }, 4400 Messages: []string{""}, 4401 Icons: []string{""}, 4402 UserProperty: []string{""}, 4403 }, 4404 cell: cell{ 4405 Result: statuspb.TestStatus_PASS, 4406 CellID: "cell-id-1", 4407 Properties: map[string]string{ 4408 "workflow-id": "run-1", 4409 "workflow-name": "//workflow-a", 4410 }, 4411 }, 4412 count: 1, 4413 expected: &statepb.Row{ 4414 Name: "test1@TESTGRID@something", 4415 Results: []int32{ 4416 int32(statuspb.TestStatus_PASS), 2, 4417 }, 4418 Messages: []string{"", ""}, 4419 Icons: []string{"", ""}, 4420 UserProperty: []string{"", ""}, 4421 }, 4422 }, 4423 } 4424 4425 for _, tc := range cases { 4426 t.Run(tc.name, func(t *testing.T) { 4427 appendCell(tc.row, tc.cell, tc.start, tc.count) 4428 sort.SliceStable(tc.row.Metric, func(i, j int) bool { 4429 return tc.row.Metric[i] < tc.row.Metric[j] 4430 }) 4431 sort.SliceStable(tc.row.Metrics, func(i, j int) bool { 4432 return tc.row.Metrics[i].Name < tc.row.Metrics[j].Name 4433 }) 4434 sort.SliceStable(tc.expected.Metric, func(i, j int) bool { 4435 return tc.expected.Metric[i] < tc.expected.Metric[j] 4436 }) 4437 sort.SliceStable(tc.expected.Metrics, func(i, j int) bool { 4438 return tc.expected.Metrics[i].Name < tc.expected.Metrics[j].Name 4439 }) 4440 if diff := cmp.Diff(tc.row, tc.expected, protocmp.Transform()); diff != "" { 4441 t.Errorf("appendCell() got unexpected diff (-got +want):\n%s", diff) 4442 } 4443 }) 4444 } 4445 } 4446 4447 // setupRow appends cells to the row. 4448 // 4449 // Auto-drops UserProperty if row.UserProperty == nil (set to empty to preserve). 4450 func setupRow(row *statepb.Row, cells ...cell) *statepb.Row { 4451 dropUserPropety := row.UserProperty == nil 4452 for idx, c := range cells { 4453 appendCell(row, c, idx, 1) 4454 } 4455 if dropUserPropety { 4456 row.UserProperty = nil 4457 } 4458 4459 return row 4460 } 4461 4462 func TestAppendColumn(t *testing.T) { 4463 cases := []struct { 4464 name string 4465 grid *statepb.Grid 4466 col inflatedColumn 4467 expected *statepb.Grid 4468 }{ 4469 { 4470 name: "append first column", 4471 grid: &statepb.Grid{}, 4472 col: inflatedColumn{Column: &statepb.Column{Build: "10"}}, 4473 expected: &statepb.Grid{ 4474 Columns: []*statepb.Column{ 4475 {Build: "10"}, 4476 }, 4477 }, 4478 }, 4479 { 4480 name: "append additional column", 4481 grid: &statepb.Grid{ 4482 Columns: []*statepb.Column{ 4483 {Build: "10"}, 4484 {Build: "11"}, 4485 }, 4486 }, 4487 col: inflatedColumn{Column: &statepb.Column{Build: "20"}}, 4488 expected: &statepb.Grid{ 4489 Columns: []*statepb.Column{ 4490 {Build: "10"}, 4491 {Build: "11"}, 4492 {Build: "20"}, 4493 }, 4494 }, 4495 }, 4496 { 4497 name: "add rows to first column", 4498 grid: &statepb.Grid{}, 4499 col: inflatedColumn{ 4500 Column: &statepb.Column{Build: "10"}, 4501 Cells: map[string]cell{ 4502 "hello": { 4503 Result: statuspb.TestStatus_PASS, 4504 CellID: "yes", 4505 Metrics: map[string]float64{ 4506 "answer": 42, 4507 }, 4508 }, 4509 "world": { 4510 Result: statuspb.TestStatus_FAIL, 4511 Message: "boom", 4512 Icon: "X", 4513 UserProperty: "prop", 4514 }, 4515 }, 4516 }, 4517 expected: &statepb.Grid{ 4518 Columns: []*statepb.Column{ 4519 {Build: "10"}, 4520 }, 4521 Rows: []*statepb.Row{ 4522 setupRow( 4523 &statepb.Row{ 4524 Name: "hello", 4525 Id: "hello", 4526 UserProperty: []string{}, 4527 }, 4528 cell{ 4529 Result: statuspb.TestStatus_PASS, 4530 CellID: "yes", 4531 Metrics: map[string]float64{"answer": 42}, 4532 }), 4533 setupRow( 4534 &statepb.Row{ 4535 Name: "world", 4536 Id: "world", 4537 UserProperty: []string{}, 4538 }, 4539 cell{ 4540 Result: statuspb.TestStatus_FAIL, 4541 Message: "boom", 4542 Icon: "X", 4543 UserProperty: "prop", 4544 }, 4545 ), 4546 }, 4547 }, 4548 }, 4549 { 4550 name: "add empty cells", 4551 grid: &statepb.Grid{ 4552 Columns: []*statepb.Column{ 4553 {Build: "10"}, 4554 {Build: "11"}, 4555 {Build: "12"}, 4556 }, 4557 Rows: []*statepb.Row{ 4558 setupRow( 4559 &statepb.Row{ 4560 Name: "deleted", 4561 UserProperty: []string{}, 4562 }, 4563 cell{Result: statuspb.TestStatus_PASS}, 4564 cell{Result: statuspb.TestStatus_PASS}, 4565 cell{Result: statuspb.TestStatus_PASS}, 4566 ), 4567 setupRow( 4568 &statepb.Row{ 4569 Name: "always", 4570 UserProperty: []string{}, 4571 }, 4572 cell{Result: statuspb.TestStatus_PASS}, 4573 cell{Result: statuspb.TestStatus_PASS}, 4574 cell{Result: statuspb.TestStatus_PASS}, 4575 ), 4576 }, 4577 }, 4578 col: inflatedColumn{ 4579 Column: &statepb.Column{Build: "20"}, 4580 Cells: map[string]cell{ 4581 "always": {Result: statuspb.TestStatus_PASS}, 4582 "new": {Result: statuspb.TestStatus_PASS}, 4583 }, 4584 }, 4585 expected: &statepb.Grid{ 4586 Columns: []*statepb.Column{ 4587 {Build: "10"}, 4588 {Build: "11"}, 4589 {Build: "12"}, 4590 {Build: "20"}, 4591 }, 4592 Rows: []*statepb.Row{ 4593 setupRow( 4594 &statepb.Row{ 4595 Name: "deleted", 4596 UserProperty: []string{}, 4597 }, 4598 cell{Result: statuspb.TestStatus_PASS}, 4599 cell{Result: statuspb.TestStatus_PASS}, 4600 cell{Result: statuspb.TestStatus_PASS}, 4601 emptyCell, 4602 ), 4603 setupRow( 4604 &statepb.Row{ 4605 Name: "always", 4606 UserProperty: []string{}, 4607 }, 4608 cell{Result: statuspb.TestStatus_PASS}, 4609 cell{Result: statuspb.TestStatus_PASS}, 4610 cell{Result: statuspb.TestStatus_PASS}, 4611 cell{Result: statuspb.TestStatus_PASS}, 4612 ), 4613 setupRow( 4614 &statepb.Row{ 4615 Name: "new", 4616 Id: "new", 4617 UserProperty: []string{}, 4618 }, 4619 emptyCell, 4620 emptyCell, 4621 emptyCell, 4622 cell{Result: statuspb.TestStatus_PASS}, 4623 ), 4624 }, 4625 }, 4626 }, 4627 } 4628 4629 for _, tc := range cases { 4630 t.Run(tc.name, func(t *testing.T) { 4631 rows := map[string]*statepb.Row{} 4632 for _, r := range tc.grid.Rows { 4633 rows[r.Name] = r 4634 } 4635 AppendColumn(tc.grid, rows, tc.col) 4636 sort.SliceStable(tc.grid.Rows, func(i, j int) bool { 4637 return tc.grid.Rows[i].Name < tc.grid.Rows[j].Name 4638 }) 4639 sort.SliceStable(tc.expected.Rows, func(i, j int) bool { 4640 return tc.expected.Rows[i].Name < tc.expected.Rows[j].Name 4641 }) 4642 if diff := cmp.Diff(&tc.expected, &tc.grid, protocmp.Transform()); diff != "" { 4643 t.Errorf("appendColumn() got unexpected diff (-want +got):\n%s", diff) 4644 } 4645 }) 4646 } 4647 } 4648 4649 func TestDynamicEmails(t *testing.T) { 4650 columnWithEmails := statepb.Column{Build: "columnWithEmail", Started: 100 - float64(0), EmailAddresses: []string{"email1@", "email2@"}} 4651 anotherColumnWithEmails := statepb.Column{Build: "anotherColumnWithEmails", Started: 100 - float64(1), EmailAddresses: []string{"email3@", "email2@"}} 4652 customColumnHeaders := map[string]string{} 4653 cases := []struct { 4654 name string 4655 row *statepb.Row 4656 columns []*statepb.Column 4657 expected *statepb.AlertInfo 4658 }{ 4659 { 4660 name: "first column with dynamic emails", 4661 row: &statepb.Row{ 4662 Results: []int32{ 4663 int32(statuspb.TestStatus_FAIL), 1, 4664 }, 4665 Messages: []string{""}, 4666 CellIds: []string{""}, 4667 }, 4668 columns: []*statepb.Column{&columnWithEmails}, 4669 expected: alertInfo(1, "", "", "", nil, &columnWithEmails, &columnWithEmails, nil, false, customColumnHeaders), 4670 }, 4671 { 4672 name: "two column with dynamic emails, we get only the first one", 4673 row: &statepb.Row{ 4674 Results: []int32{ 4675 int32(statuspb.TestStatus_FAIL), 2, 4676 }, 4677 Messages: []string{"", ""}, 4678 CellIds: []string{"", ""}, 4679 }, 4680 columns: []*statepb.Column{&anotherColumnWithEmails, &columnWithEmails}, 4681 expected: alertInfo(2, "", "", "", nil, &columnWithEmails, &anotherColumnWithEmails, nil, false, customColumnHeaders), 4682 }, 4683 { 4684 name: "first column don't have results, second column emails on the alert", 4685 row: &statepb.Row{ 4686 Results: []int32{ 4687 int32(statuspb.TestStatus_NO_RESULT), 1, 4688 int32(statuspb.TestStatus_FAIL), 1, 4689 }, 4690 Messages: []string{"", ""}, 4691 CellIds: []string{"", ""}, 4692 }, 4693 columns: []*statepb.Column{&columnWithEmails, &anotherColumnWithEmails}, 4694 expected: alertInfo(1, "", "", "", nil, &anotherColumnWithEmails, &anotherColumnWithEmails, nil, false, customColumnHeaders), 4695 }, 4696 } 4697 for _, tc := range cases { 4698 defaultColumnHeaders := []*configpb.TestGroup_ColumnHeader{} 4699 actual := alertRow(tc.columns, tc.row, 1, 1, false, "", defaultColumnHeaders) 4700 if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" { 4701 t.Errorf("alertRow() not as expected (-want, +got): %s", diff) 4702 } 4703 } 4704 } 4705 4706 func TestAlertRow(t *testing.T) { 4707 var columns []*statepb.Column 4708 for i, id := range []string{"a", "b", "c", "d", "e", "f"} { 4709 columns = append(columns, &statepb.Column{ 4710 Build: id, 4711 Started: 100 - float64(i), 4712 Extra: []string{ 4713 "world", 4714 "bar", 4715 }, 4716 }) 4717 } 4718 defaultColumnHeaders := []*configpb.TestGroup_ColumnHeader{ 4719 { 4720 Property: "hello", 4721 }, 4722 { 4723 Property: "foo", 4724 }, 4725 } 4726 customColumnHeaders := map[string]string{ 4727 "hello": "world", 4728 "foo": "bar", 4729 } 4730 cases := []struct { 4731 name string 4732 row *statepb.Row 4733 failOpen int 4734 passClose int 4735 property string 4736 columnHeader []*configpb.TestGroup_ColumnHeader 4737 expected *statepb.AlertInfo 4738 }{ 4739 { 4740 name: "never alert by default", 4741 row: &statepb.Row{ 4742 Results: []int32{ 4743 int32(statuspb.TestStatus_FAIL), 6, 4744 }, 4745 }, 4746 }, 4747 { 4748 name: "passes do not alert", 4749 row: &statepb.Row{ 4750 Results: []int32{ 4751 int32(statuspb.TestStatus_PASS), 6, 4752 }, 4753 }, 4754 failOpen: 1, 4755 passClose: 3, 4756 }, 4757 { 4758 name: "flakes do not alert", 4759 row: &statepb.Row{ 4760 Results: []int32{ 4761 int32(statuspb.TestStatus_FLAKY), 6, 4762 }, 4763 }, 4764 failOpen: 1, 4765 }, 4766 { 4767 name: "intermittent failures do not alert", 4768 row: &statepb.Row{ 4769 Results: []int32{ 4770 int32(statuspb.TestStatus_FAIL), 2, 4771 int32(statuspb.TestStatus_PASS), 1, 4772 int32(statuspb.TestStatus_FAIL), 2, 4773 }, 4774 }, 4775 failOpen: 3, 4776 }, 4777 { 4778 name: "new failures alert", 4779 columnHeader: defaultColumnHeaders, 4780 row: &statepb.Row{ 4781 Results: []int32{ 4782 int32(statuspb.TestStatus_FAIL), 3, 4783 int32(statuspb.TestStatus_PASS), 3, 4784 }, 4785 Messages: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4786 CellIds: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4787 }, 4788 failOpen: 3, 4789 expected: alertInfo(3, "no", "very wrong", "no", nil, columns[2], columns[0], columns[3], false, customColumnHeaders), 4790 }, 4791 { 4792 name: "rows without cell IDs can alert", 4793 row: &statepb.Row{ 4794 Results: []int32{ 4795 int32(statuspb.TestStatus_FAIL), 3, 4796 int32(statuspb.TestStatus_PASS), 3, 4797 }, 4798 Messages: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4799 }, 4800 failOpen: 3, 4801 expected: alertInfo(3, "no", "", "", nil, columns[2], columns[0], columns[3], false, nil), 4802 }, 4803 { 4804 name: "too few passes do not close", 4805 columnHeader: defaultColumnHeaders, 4806 row: &statepb.Row{ 4807 Results: []int32{ 4808 int32(statuspb.TestStatus_PASS), 2, 4809 int32(statuspb.TestStatus_FAIL), 4, 4810 }, 4811 Messages: []string{"nope", "no", "yay", "very wrong", "hi", "hello"}, 4812 CellIds: []string{"wrong", "no", "yep", "very wrong", "hi", "hello"}, 4813 }, 4814 failOpen: 1, 4815 passClose: 3, 4816 expected: alertInfo(4, "yay", "hello", "yep", nil, columns[5], columns[2], nil, false, customColumnHeaders), 4817 }, 4818 { 4819 name: "flakes do not close", 4820 columnHeader: defaultColumnHeaders, 4821 row: &statepb.Row{ 4822 Results: []int32{ 4823 int32(statuspb.TestStatus_FLAKY), 2, 4824 int32(statuspb.TestStatus_FAIL), 4, 4825 }, 4826 Messages: []string{"nope", "no", "yay", "very wrong", "hi", "hello"}, 4827 CellIds: []string{"wrong", "no", "yep", "very wrong", "hi", "hello"}, 4828 }, 4829 failOpen: 1, 4830 expected: alertInfo(4, "yay", "hello", "yep", nil, columns[5], columns[2], nil, false, customColumnHeaders), 4831 }, 4832 { 4833 name: "failures after insufficient passes", 4834 row: &statepb.Row{ 4835 Results: []int32{ 4836 int32(statuspb.TestStatus_FAIL), 1, 4837 int32(statuspb.TestStatus_FLAKY), 1, 4838 int32(statuspb.TestStatus_FAIL), 1, 4839 int32(statuspb.TestStatus_PASS), 1, 4840 int32(statuspb.TestStatus_FAIL), 2, 4841 }, 4842 Messages: []string{"newest-fail", "what", "carelessness", "okay", "alert-here", "misfortune"}, 4843 CellIds: []string{"f0", "flake", "f2", "p3", "f4", "f5"}, 4844 }, 4845 failOpen: 2, 4846 passClose: 2, 4847 expected: alertInfo(4, "newest-fail", "f5", "f0", nil, columns[5], columns[0], nil, false, nil), 4848 }, 4849 { 4850 name: "close alert", 4851 row: &statepb.Row{ 4852 Results: []int32{ 4853 int32(statuspb.TestStatus_PASS), 1, 4854 int32(statuspb.TestStatus_FAIL), 5, 4855 }, 4856 }, 4857 failOpen: 1, 4858 }, 4859 { 4860 name: "track through empty results", 4861 row: &statepb.Row{ 4862 Results: []int32{ 4863 int32(statuspb.TestStatus_FAIL), 1, 4864 int32(statuspb.TestStatus_NO_RESULT), 1, 4865 int32(statuspb.TestStatus_FAIL), 4, 4866 }, 4867 Messages: []string{"yay" /*no result */, "no", "buu", "wrong", "nono"}, 4868 CellIds: []string{"yay-cell" /*no result */, "no", "buzz", "wrong2", "nada"}, 4869 }, 4870 failOpen: 5, 4871 passClose: 2, 4872 expected: alertInfo(5, "yay", "nada", "yay-cell", nil, columns[5], columns[0], nil, false, nil), 4873 }, 4874 { 4875 name: "track passes through empty results", 4876 row: &statepb.Row{ 4877 Results: []int32{ 4878 int32(statuspb.TestStatus_PASS), 1, 4879 int32(statuspb.TestStatus_NO_RESULT), 1, 4880 int32(statuspb.TestStatus_PASS), 1, 4881 int32(statuspb.TestStatus_FAIL), 3, 4882 }, 4883 }, 4884 failOpen: 1, 4885 passClose: 2, 4886 }, 4887 { 4888 name: "running cells advance compressed index", 4889 row: &statepb.Row{ 4890 Results: []int32{ 4891 int32(statuspb.TestStatus_RUNNING), 1, 4892 int32(statuspb.TestStatus_FAIL), 5, 4893 }, 4894 Messages: []string{"running0", "fail1-expected", "fail2", "fail3", "fail4", "fail5"}, 4895 CellIds: []string{"wrong", "yep", "no2", "no3", "no4", "no5"}, 4896 }, 4897 failOpen: 1, 4898 expected: alertInfo(5, "fail1-expected", "no5", "yep", nil, columns[5], columns[1], nil, false, nil), 4899 }, 4900 { 4901 name: "complex", 4902 columnHeader: defaultColumnHeaders, 4903 row: &statepb.Row{ 4904 Results: []int32{ 4905 int32(statuspb.TestStatus_PASS), 1, 4906 int32(statuspb.TestStatus_FAIL), 1, 4907 int32(statuspb.TestStatus_PASS), 1, 4908 int32(statuspb.TestStatus_FAIL), 2, 4909 int32(statuspb.TestStatus_PASS), 1, 4910 }, 4911 Messages: []string{"latest pass", "latest fail", "pass", "second fail", "first fail", "first pass"}, 4912 CellIds: []string{"no-p0", "no-f1", "no-p2", "no-f3", "yes-f4", "yes-p5"}, 4913 }, 4914 failOpen: 2, 4915 passClose: 2, 4916 expected: alertInfo(3, "latest fail", "yes-f4", "no-f1", nil, columns[4], columns[1], columns[5], false, customColumnHeaders), 4917 }, 4918 { 4919 name: "alert consecutive failures only", 4920 row: &statepb.Row{ 4921 Results: []int32{ 4922 int32(statuspb.TestStatus_PASS), 1, 4923 int32(statuspb.TestStatus_FAIL), 1, 4924 int32(statuspb.TestStatus_PASS), 1, 4925 int32(statuspb.TestStatus_FAIL), 1, 4926 int32(statuspb.TestStatus_PASS), 3, 4927 }, 4928 Messages: []string{"latest pass", "latest fail", "pass", "second fail", "pass", "pass", "pass"}, 4929 CellIds: []string{"p0", "f1", "p2", "f3", "p4", "p5", "p6"}, 4930 }, 4931 failOpen: 2, 4932 passClose: 3, 4933 }, 4934 { 4935 name: "properties", 4936 row: &statepb.Row{ 4937 Results: []int32{ 4938 int32(statuspb.TestStatus_FAIL), 3, 4939 int32(statuspb.TestStatus_PASS), 3, 4940 }, 4941 Messages: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4942 CellIds: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4943 UserProperty: []string{"prop0", "prop1", "prop2", "prop3", "prop4", "prop5"}, 4944 }, 4945 failOpen: 3, 4946 property: "some-prop", 4947 expected: alertInfo(3, "no", "very wrong", "no", map[string]string{"some-prop": "prop0"}, columns[2], columns[0], columns[3], false, nil), 4948 }, 4949 { 4950 name: "properties after passes", 4951 columnHeader: defaultColumnHeaders, 4952 row: &statepb.Row{ 4953 Results: []int32{ 4954 int32(statuspb.TestStatus_PASS), 2, 4955 int32(statuspb.TestStatus_FAIL), 3, 4956 int32(statuspb.TestStatus_PASS), 1, 4957 }, 4958 Messages: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4959 CellIds: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4960 UserProperty: []string{"prop0", "prop1", "prop2", "prop3", "prop4", "prop5"}, 4961 }, 4962 failOpen: 3, 4963 passClose: 3, 4964 property: "some-prop", 4965 expected: alertInfo(3, "very wrong", "hi", "very wrong", map[string]string{"some-prop": "prop2"}, columns[4], columns[2], columns[5], false, customColumnHeaders), 4966 }, 4967 { 4968 name: "empty properties", 4969 row: &statepb.Row{ 4970 Results: []int32{ 4971 int32(statuspb.TestStatus_FAIL), 3, 4972 int32(statuspb.TestStatus_PASS), 3, 4973 }, 4974 Messages: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4975 CellIds: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4976 UserProperty: []string{}, 4977 }, 4978 failOpen: 3, 4979 property: "some-prop", 4980 expected: alertInfo(3, "no", "very wrong", "no", nil, columns[2], columns[0], columns[3], false, nil), 4981 }, 4982 { 4983 name: "insufficient properties", 4984 row: &statepb.Row{ 4985 Results: []int32{ 4986 int32(statuspb.TestStatus_PASS), 2, 4987 int32(statuspb.TestStatus_FAIL), 4, 4988 }, 4989 Messages: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4990 CellIds: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 4991 UserProperty: []string{"prop0"}, 4992 }, 4993 failOpen: 3, 4994 passClose: 3, 4995 property: "some-prop", 4996 expected: alertInfo(4, "very wrong", "hello", "very wrong", nil, columns[5], columns[2], nil, false, nil), 4997 }, 4998 { 4999 name: "insufficient column header values", 5000 columnHeader: append( 5001 defaultColumnHeaders, 5002 &configpb.TestGroup_ColumnHeader{ 5003 Property: "extra-key", 5004 }, 5005 ), 5006 row: &statepb.Row{ 5007 Results: []int32{ 5008 int32(statuspb.TestStatus_PASS), 2, 5009 int32(statuspb.TestStatus_FAIL), 4, 5010 }, 5011 Messages: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 5012 CellIds: []string{"no", "no again", "very wrong", "yes", "hi", "hello"}, 5013 UserProperty: []string{"prop0"}, 5014 }, 5015 failOpen: 3, 5016 passClose: 3, 5017 property: "some-prop", 5018 expected: alertInfo(4, "very wrong", "hello", "very wrong", nil, columns[5], columns[2], nil, false, customColumnHeaders), 5019 }, 5020 } 5021 5022 for _, tc := range cases { 5023 t.Run(tc.name, func(t *testing.T) { 5024 actual := alertRow(columns, tc.row, tc.failOpen, tc.passClose, false, tc.property, tc.columnHeader) 5025 if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" { 5026 t.Errorf("alertRow() not as expected (-want, +got): %s", diff) 5027 } 5028 }) 5029 } 5030 } 5031 5032 func TestBuildID(t *testing.T) { 5033 cases := []struct { 5034 name string 5035 build string 5036 extra string 5037 useCommit bool 5038 expected string 5039 }{ 5040 { 5041 name: "return empty by default", 5042 }, 5043 { 5044 name: "use header as commit", 5045 build: "wrong", 5046 extra: "right", 5047 useCommit: true, 5048 expected: "right", 5049 }, 5050 { 5051 name: "use build otherwise", 5052 build: "right", 5053 extra: "wrong", 5054 expected: "right", 5055 }, 5056 } 5057 5058 for _, tc := range cases { 5059 t.Run(tc.name, func(t *testing.T) { 5060 col := statepb.Column{ 5061 Build: tc.build, 5062 } 5063 if tc.extra != "" { 5064 col.Extra = append(col.Extra, tc.extra) 5065 } 5066 if actual := buildID(&col, tc.useCommit); actual != tc.expected { 5067 t.Errorf("%q != expected %q", actual, tc.expected) 5068 } 5069 }) 5070 } 5071 } 5072 5073 func TestStamp(t *testing.T) { 5074 cases := []struct { 5075 name string 5076 col *statepb.Column 5077 expected *timestamp.Timestamp 5078 }{ 5079 { 5080 name: "0 returns nil", 5081 }, 5082 { 5083 name: "no nanos", 5084 col: &statepb.Column{ 5085 Started: 2000, 5086 }, 5087 expected: ×tamp.Timestamp{ 5088 Seconds: 2, 5089 Nanos: 0, 5090 }, 5091 }, 5092 { 5093 name: "milli to nano", 5094 col: &statepb.Column{ 5095 Started: 1234, 5096 }, 5097 expected: ×tamp.Timestamp{ 5098 Seconds: 1, 5099 Nanos: 234000000, 5100 }, 5101 }, 5102 { 5103 name: "double to nanos", 5104 col: &statepb.Column{ 5105 Started: 1.1, 5106 }, 5107 expected: ×tamp.Timestamp{ 5108 Seconds: 0, 5109 Nanos: 1100000, 5110 }, 5111 }, 5112 } 5113 5114 for _, tc := range cases { 5115 t.Run(tc.name, func(t *testing.T) { 5116 if actual := stamp(tc.col); !reflect.DeepEqual(actual, tc.expected) { 5117 t.Errorf("stamp %s != expected stamp %s", actual, tc.expected) 5118 } 5119 }) 5120 } 5121 } 5122 5123 func TestDropEmptyRows(t *testing.T) { 5124 cases := []struct { 5125 name string 5126 results map[string][]int32 5127 expected map[string][]int32 5128 }{ 5129 { 5130 name: "basically works", 5131 expected: map[string][]int32{}, 5132 }, 5133 { 5134 name: "keep everything", 5135 results: map[string][]int32{ 5136 "pass": {int32(statuspb.TestStatus_PASS), 1}, 5137 "fail": {int32(statuspb.TestStatus_FAIL), 2}, 5138 "running": {int32(statuspb.TestStatus_RUNNING), 3}, 5139 }, 5140 expected: map[string][]int32{ 5141 "pass": {int32(statuspb.TestStatus_PASS), 1}, 5142 "fail": {int32(statuspb.TestStatus_FAIL), 2}, 5143 "running": {int32(statuspb.TestStatus_RUNNING), 3}, 5144 }, 5145 }, 5146 { 5147 name: "keep mixture", 5148 results: map[string][]int32{ 5149 "was empty": { 5150 int32(statuspb.TestStatus_PASS), 1, 5151 int32(statuspb.TestStatus_NO_RESULT), 1, 5152 }, 5153 "now empty": { 5154 int32(statuspb.TestStatus_NO_RESULT), 2, 5155 int32(statuspb.TestStatus_FAIL), 2, 5156 }, 5157 }, 5158 expected: map[string][]int32{ 5159 "was empty": { 5160 int32(statuspb.TestStatus_PASS), 1, 5161 int32(statuspb.TestStatus_NO_RESULT), 1, 5162 }, 5163 "now empty": { 5164 int32(statuspb.TestStatus_NO_RESULT), 2, 5165 int32(statuspb.TestStatus_FAIL), 2, 5166 }, 5167 }, 5168 }, 5169 { 5170 name: "drop everything", 5171 results: map[string][]int32{ 5172 "drop": {int32(statuspb.TestStatus_NO_RESULT), 1}, 5173 "gone": {int32(statuspb.TestStatus_NO_RESULT), 10}, 5174 "poof": {int32(statuspb.TestStatus_NO_RESULT), 100}, 5175 }, 5176 expected: map[string][]int32{}, 5177 }, 5178 } 5179 5180 for _, tc := range cases { 5181 t.Run(tc.name, func(t *testing.T) { 5182 var grid statepb.Grid 5183 rows := make(map[string]*statepb.Row, len(tc.results)) 5184 for name, res := range tc.results { 5185 r := &statepb.Row{Name: name} 5186 r.Results = res 5187 grid.Rows = append(grid.Rows, r) 5188 rows[name] = r 5189 } 5190 dropEmptyRows(logrus.WithField("name", tc.name), &grid, rows) 5191 actualRowMap := make(map[string]*statepb.Row, len(grid.Rows)) 5192 for _, r := range grid.Rows { 5193 actualRowMap[r.Name] = r 5194 } 5195 5196 if diff := cmp.Diff(rows, actualRowMap, protocmp.Transform()); diff != "" { 5197 t.Fatalf("dropEmptyRows() unmatched row maps (-grid, +map):\n%s", diff) 5198 } 5199 5200 actual := make(map[string][]int32, len(rows)) 5201 for name, row := range rows { 5202 actual[name] = row.Results 5203 } 5204 5205 if diff := cmp.Diff(actual, tc.expected, protocmp.Transform()); diff != "" { 5206 t.Errorf("dropEmptyRows() got unexpected diff (-have, +want):\n%s", diff) 5207 } 5208 }) 5209 } 5210 } 5211 5212 func TestTruncate(t *testing.T) { 5213 cases := []struct { 5214 name string 5215 msg string 5216 max int 5217 want string 5218 }{ 5219 { 5220 name: "empty", 5221 msg: "", 5222 max: 20, 5223 want: "", 5224 }, 5225 { 5226 name: "short", 5227 msg: "short message", 5228 max: 20, 5229 want: "short message", 5230 }, 5231 { 5232 name: "long", 5233 msg: "i'm too long of a message, oh no what will i do", 5234 max: 20, 5235 want: "i'm too lo... will i do", 5236 }, 5237 { 5238 name: "long runes", 5239 msg: "庭には二羽鶏がいる。", // In the yard two chickens are there. 5240 max: 20, 5241 want: "庭には...いる。", 5242 }, 5243 { 5244 name: "short runes", 5245 msg: "鶏がいる。", // Two chickens are there. 5246 max: 20, 5247 want: "鶏がいる。", 5248 }, 5249 { 5250 name: "small max", 5251 msg: "short message", 5252 max: 2, 5253 want: "s...e", 5254 }, 5255 { 5256 name: "odd max", 5257 msg: "short message", 5258 max: 5, 5259 want: "sh...ge", 5260 }, 5261 { 5262 name: "max 1", 5263 msg: "short message", 5264 max: 1, 5265 want: "...", 5266 }, 5267 { 5268 name: "max 0", 5269 msg: "short message", 5270 max: 0, 5271 want: "short message", 5272 }, 5273 } 5274 5275 for _, tc := range cases { 5276 t.Run(tc.name, func(t *testing.T) { 5277 if got := truncate(tc.msg, tc.max); got != tc.want { 5278 t.Errorf("truncate(%q, %d) got %q, want %q", tc.msg, tc.max, got, tc.want) 5279 } 5280 }) 5281 } 5282 } 5283 5284 func TestHotlistIDs(t *testing.T) { 5285 cases := []struct { 5286 name string 5287 hotlistIDs string 5288 want []string 5289 }{ 5290 { 5291 name: "none", 5292 hotlistIDs: "", 5293 want: nil, 5294 }, 5295 { 5296 name: "empty", 5297 hotlistIDs: ",,", 5298 want: nil, 5299 }, 5300 { 5301 name: "one", 5302 hotlistIDs: "123", 5303 want: []string{"123"}, 5304 }, 5305 { 5306 name: "many", 5307 hotlistIDs: "123,456,789", 5308 want: []string{"123", "456", "789"}, 5309 }, 5310 { 5311 name: "spaces", 5312 hotlistIDs: "123 , 456, 789 ", 5313 want: []string{"123", "456", "789"}, 5314 }, 5315 { 5316 name: "many empty", 5317 hotlistIDs: "123,,456,", 5318 want: []string{"123", "456"}, 5319 }, 5320 { 5321 name: "complex", 5322 hotlistIDs: " 123,456,,, 789 ,", 5323 want: []string{"123", "456", "789"}, 5324 }, 5325 } 5326 5327 for _, tc := range cases { 5328 t.Run(tc.name, func(t *testing.T) { 5329 col := &statepb.Column{ 5330 HotlistIds: tc.hotlistIDs, 5331 } 5332 got := hotlistIDs(col) 5333 if diff := cmp.Diff(tc.want, got); diff != "" { 5334 t.Errorf("hotlistIDs(%v) differed (-want, +got): %s", col, diff) 5335 } 5336 }) 5337 } 5338 } 5339 5340 func TestTruncateLastColumn(t *testing.T) { 5341 cases := []struct { 5342 name string 5343 grid []InflatedColumn 5344 expect []InflatedColumn 5345 }{ 5346 { 5347 name: "empty grid", 5348 grid: []InflatedColumn{}, 5349 expect: []inflatedColumn{}, 5350 }, 5351 { 5352 name: "nil grid", 5353 grid: nil, 5354 expect: nil, 5355 }, 5356 { 5357 name: "grid", 5358 grid: []InflatedColumn{ 5359 { 5360 Cells: map[string]Cell{ 5361 "row_1": { 5362 ID: "row_1", 5363 Result: statuspb.TestStatus_PASS, 5364 }, 5365 "row_2": { 5366 ID: "row_2", 5367 Result: statuspb.TestStatus_FAIL, 5368 }, 5369 }, 5370 }, 5371 { 5372 Cells: map[string]Cell{ 5373 "row_1": { 5374 ID: "row_1", 5375 Result: statuspb.TestStatus_PASS, 5376 }, 5377 "row_2": { 5378 ID: "row_2", 5379 Result: statuspb.TestStatus_PASS, 5380 }, 5381 }, 5382 }, 5383 }, 5384 expect: []InflatedColumn{ 5385 { 5386 Cells: map[string]Cell{ 5387 "row_1": { 5388 ID: "row_1", 5389 Result: statuspb.TestStatus_PASS, 5390 }, 5391 "row_2": { 5392 ID: "row_2", 5393 Result: statuspb.TestStatus_FAIL, 5394 }, 5395 }, 5396 }, 5397 { 5398 Cells: map[string]Cell{ 5399 "row_1": { 5400 ID: "row_1", 5401 Result: statuspb.TestStatus_UNKNOWN, 5402 Icon: "...", 5403 Message: "100 candy grid exceeds maximum size of 10 candys", 5404 }, 5405 "row_2": { 5406 ID: "row_2", 5407 Result: statuspb.TestStatus_UNKNOWN, 5408 Icon: "...", 5409 Message: "100 candy grid exceeds maximum size of 10 candys", 5410 }, 5411 }, 5412 }, 5413 }, 5414 }, 5415 } 5416 5417 for _, tc := range cases { 5418 t.Run(tc.name, func(t *testing.T) { 5419 actual := tc.grid 5420 truncateLastColumn(actual, 100, 10, "candy") 5421 5422 if diff := cmp.Diff(actual, tc.expect); diff != "" { 5423 t.Error("mismatch (+got, -want)") 5424 t.Log(diff) 5425 } 5426 }) 5427 } 5428 }