github.com/GoogleCloudPlatform/testgrid@v0.0.174/config/config_test.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package config 18 19 import ( 20 "errors" 21 "reflect" 22 "strings" 23 "testing" 24 25 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 26 multierror "github.com/hashicorp/go-multierror" 27 ) 28 29 func TestNormalize(t *testing.T) { 30 tests := []struct { 31 input string 32 expected string 33 }{ 34 { 35 input: "normal", 36 expected: "normal", 37 }, 38 { 39 input: "UPPER", 40 expected: "upper", 41 }, 42 { 43 input: "pun-_*ctuation Y_E_A_H!", 44 expected: "punctuationyeah", 45 }, 46 } 47 48 for _, test := range tests { 49 t.Run(test.input, func(t *testing.T) { 50 got := Normalize(test.input) 51 if got != test.expected { 52 t.Fatalf("got %s, want %s", got, test.expected) 53 } 54 }) 55 } 56 } 57 58 func TestValidateUnique(t *testing.T) { 59 tests := []struct { 60 name string 61 input []string 62 expectedErrs []error 63 }{ 64 { 65 name: "No names", 66 input: []string{}, 67 }, 68 { 69 name: "Unique names", 70 input: []string{"test_group_1", "test_group_2", "test_group_3"}, 71 }, 72 { 73 name: "Duplicate name; error", 74 input: []string{"test_group_1", "test_group_1"}, 75 expectedErrs: []error{ 76 DuplicateNameError{"testgroup1", "TestGroup"}, 77 }, 78 }, 79 { 80 name: "Duplicate name after normalization; error", 81 input: []string{"test_group_1", "TEST GROUP 1"}, 82 expectedErrs: []error{ 83 DuplicateNameError{"testgroup1", "TestGroup"}, 84 }, 85 }, 86 } 87 for _, test := range tests { 88 t.Run(test.name, func(t *testing.T) { 89 err := validateUnique(test.input, "TestGroup") 90 if err == nil { 91 if len(test.expectedErrs) > 0 { 92 t.Fatalf("Expected %v, but got no error", test.expectedErrs) 93 } 94 } else { 95 if len(test.expectedErrs) == 0 { 96 t.Fatalf("Unexpected Error: %v", err) 97 } 98 99 if mErr, ok := err.(*multierror.Error); ok { 100 if !reflect.DeepEqual(test.expectedErrs, mErr.Errors) { 101 t.Fatalf("Expected %v, but got: %v", test.expectedErrs, mErr.Errors) 102 } 103 } else { 104 t.Fatalf("Expected %v, but got: %v", test.expectedErrs, err) 105 } 106 } 107 }) 108 } 109 } 110 111 func TestValidateAllUnique(t *testing.T) { 112 cases := []struct { 113 name string 114 c *configpb.Configuration 115 pass bool 116 }{ 117 { 118 name: "reject nil config.Configuration", 119 c: nil, 120 pass: false, 121 }, 122 { 123 name: "everything works", 124 c: &configpb.Configuration{ 125 TestGroups: []*configpb.TestGroup{ 126 { 127 Name: "test_group_1", 128 }, 129 }, 130 Dashboards: []*configpb.Dashboard{ 131 { 132 Name: "dash", 133 DashboardTab: []*configpb.DashboardTab{ 134 { 135 Name: "tab_1", 136 }, 137 }, 138 }, 139 }, 140 DashboardGroups: []*configpb.DashboardGroup{ 141 { 142 Name: "dash_group_1", 143 }, 144 }, 145 }, 146 pass: true, 147 }, 148 { 149 name: "reject empty group names", 150 c: &configpb.Configuration{ 151 TestGroups: []*configpb.TestGroup{ 152 {}, 153 }, 154 }, 155 }, 156 { 157 name: "reject empty dashboard names", 158 c: &configpb.Configuration{ 159 Dashboards: []*configpb.Dashboard{ 160 {}, 161 }, 162 }, 163 }, 164 { 165 name: "reject empty tab names", 166 c: &configpb.Configuration{ 167 Dashboards: []*configpb.Dashboard{ 168 { 169 Name: "dash_1", 170 DashboardTab: []*configpb.DashboardTab{ 171 {}, 172 }, 173 }, 174 }, 175 }, 176 }, 177 { 178 name: "reject empty dashboard group names", 179 c: &configpb.Configuration{ 180 DashboardGroups: []*configpb.DashboardGroup{ 181 {}, 182 }, 183 }, 184 }, 185 { 186 name: "dashboard group names cannot match a dashboard name", 187 c: &configpb.Configuration{ 188 Dashboards: []*configpb.Dashboard{ 189 { 190 Name: "foo", 191 }, 192 }, 193 DashboardGroups: []*configpb.DashboardGroup{ 194 { 195 Name: "foo", 196 }, 197 }, 198 }, 199 }, 200 } 201 202 for _, tc := range cases { 203 t.Run(tc.name, func(t *testing.T) { 204 err := validateAllUnique(tc.c) 205 switch { 206 case err != nil: 207 if tc.pass { 208 t.Errorf("got unexpected error: %v", err) 209 } 210 case !tc.pass: 211 t.Error("failed to get an error") 212 } 213 214 }) 215 } 216 } 217 218 func TestValidateReferencesExist(t *testing.T) { 219 tests := []struct { 220 name string 221 input *configpb.Configuration 222 expectedErrs []error 223 }{ 224 { 225 name: "reject nil config.Configuration", 226 input: nil, 227 expectedErrs: []error{errors.New("got an empty config.Configuration")}, 228 }, 229 { 230 name: "Dashboard Tabs must reference an existing Test Group", 231 input: &configpb.Configuration{ 232 Dashboards: []*configpb.Dashboard{ 233 { 234 Name: "dash_1", 235 DashboardTab: []*configpb.DashboardTab{ 236 { 237 Name: "tab_1", 238 TestGroupName: "test_group_1", 239 }, 240 { 241 Name: "tab_2", 242 TestGroupName: "test_group_2", 243 }, 244 }, 245 }, 246 }, 247 TestGroups: []*configpb.TestGroup{ 248 { 249 Name: "test_group_1", 250 }, 251 }, 252 }, 253 expectedErrs: []error{ 254 MissingEntityError{"test_group_2", "TestGroup"}, 255 }, 256 }, 257 { 258 name: "Test Groups must have an associated Dashboard Tab", 259 input: &configpb.Configuration{ 260 Dashboards: []*configpb.Dashboard{ 261 { 262 Name: "dash_1", 263 DashboardTab: []*configpb.DashboardTab{}, 264 }, 265 }, 266 TestGroups: []*configpb.TestGroup{ 267 { 268 Name: "test_group_1", 269 }, 270 }, 271 }, 272 expectedErrs: []error{ 273 ValidationError{"test_group_1", "TestGroup", "Each Test Group must be referenced by at least 1 Dashboard Tab."}, 274 }, 275 }, 276 { 277 name: "Dashboard Groups must reference existing Dashboards", 278 input: &configpb.Configuration{ 279 Dashboards: []*configpb.Dashboard{ 280 { 281 Name: "dash_1", 282 DashboardTab: []*configpb.DashboardTab{ 283 { 284 Name: "tab_1", 285 TestGroupName: "test_group_1", 286 }, 287 }, 288 }, 289 }, 290 TestGroups: []*configpb.TestGroup{ 291 { 292 Name: "test_group_1", 293 }, 294 }, 295 DashboardGroups: []*configpb.DashboardGroup{ 296 { 297 Name: "dash_group_1", 298 DashboardNames: []string{"dash_1", "dash_2", "dash_3"}, 299 }, 300 }, 301 }, 302 expectedErrs: []error{ 303 MissingEntityError{"dash_2", "Dashboard"}, 304 MissingEntityError{"dash_3", "Dashboard"}, 305 }, 306 }, 307 { 308 name: "A Dashboard can belong to at most 1 Dashboard Group", 309 input: &configpb.Configuration{ 310 Dashboards: []*configpb.Dashboard{ 311 { 312 Name: "dash_1", 313 DashboardTab: []*configpb.DashboardTab{ 314 { 315 Name: "tab_1", 316 TestGroupName: "test_group_1", 317 }, 318 }, 319 }, 320 }, 321 TestGroups: []*configpb.TestGroup{ 322 { 323 Name: "test_group_1", 324 }, 325 }, 326 DashboardGroups: []*configpb.DashboardGroup{ 327 { 328 Name: "dash_group_1", 329 DashboardNames: []string{"dash_1"}, 330 }, 331 { 332 Name: "dash_group_2", 333 DashboardNames: []string{"dash_1"}, 334 }, 335 }, 336 }, 337 expectedErrs: []error{ 338 ValidationError{"dash_1", "Dashboard", "A Dashboard cannot be in more than 1 Dashboard Group."}, 339 }, 340 }, 341 } 342 343 for _, test := range tests { 344 t.Run(test.name, func(t *testing.T) { 345 err := validateReferencesExist(test.input) 346 if err != nil && len(test.expectedErrs) == 0 { 347 t.Fatalf("Unexpected Error: %v", err) 348 } 349 350 if len(test.expectedErrs) != 0 { 351 if err == nil { 352 t.Fatalf("Expected %v, but got no error", test.expectedErrs) 353 } 354 355 if mErr, ok := err.(*multierror.Error); ok { 356 if !reflect.DeepEqual(test.expectedErrs, mErr.Errors) { 357 t.Fatalf("Expected %v, but got: %v", test.expectedErrs, mErr.Errors) 358 } 359 } else { 360 t.Fatalf("Expected %v, but got: %v", test.expectedErrs, err) 361 } 362 } 363 }) 364 } 365 } 366 367 func TestValidateName(t *testing.T) { 368 stringOfLength := func(length int) string { 369 var sb strings.Builder 370 for i := 0; i < length; i++ { 371 sb.WriteRune('a') 372 } 373 return sb.String() 374 } 375 376 tests := []struct { 377 name string 378 input string 379 pass bool 380 }{ 381 { 382 name: "Names can't be empty", 383 input: "", 384 }, 385 { 386 name: "Invalid characters are filtered out", 387 input: "___%%%***!!!???'''|||@@@###$$$^^^///\\\\\\", 388 }, 389 { 390 name: "Names can't be too short", 391 input: "q", 392 }, 393 { 394 name: "Names must contain 3+ alphanumeric characters", 395 input: "?rs=%%", 396 }, 397 { 398 name: "Names can't be too long", 399 input: stringOfLength(2049), 400 }, 401 { 402 name: "Names can't start with dashboard", 403 input: "dashboard", 404 }, 405 { 406 name: "Names can't start with summary", 407 input: "_summary_", 408 }, 409 { 410 name: "Names can't start with alerter", 411 input: "ALERTER", 412 }, 413 { 414 name: "Names can't start with bugs", 415 input: "bugs-1-2-3", 416 }, 417 { 418 name: "Names may contain forbidden prefixes in the middle", 419 input: "file-bugs-for-alerter", 420 pass: true, 421 }, 422 { 423 name: "weird characters", 424 input: "[my] dash/tab (this_poem.of-sorts~) <@special1>", 425 pass: true, 426 }, 427 { 428 name: "backslash", 429 input: "my\\dash", 430 }, 431 { 432 name: "colon", 433 input: "my:dash", 434 }, 435 { 436 name: "question", 437 input: "my?dash", 438 }, 439 { 440 name: "semicolon", 441 input: "my;dash", 442 }, 443 { 444 name: "Valid name", 445 input: "some-test-group", 446 pass: true, 447 }, 448 } 449 450 for _, test := range tests { 451 t.Run(test.name, func(t *testing.T) { 452 err := validateName(test.input) 453 pass := err == nil 454 if pass != test.pass { 455 t.Fatalf("name %s got pass = %v, want pass = %v", test.input, pass, test.pass) 456 } 457 }) 458 } 459 } 460 461 func TestValidateResultStoreSource(t *testing.T) { 462 tests := []struct { 463 name string 464 tg *configpb.TestGroup 465 err bool 466 }{ 467 { 468 name: "nil test group", 469 tg: nil, 470 err: false, 471 }, 472 { 473 name: "empty test group", 474 tg: &configpb.TestGroup{}, 475 err: false, 476 }, 477 { 478 name: "empty ResultStore source", 479 tg: &configpb.TestGroup{ 480 ResultSource: &configpb.TestGroup_ResultSource{ 481 ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{ 482 ResultstoreConfig: &configpb.ResultStoreConfig{}, 483 }, 484 }, 485 }, 486 err: true, 487 }, 488 { 489 name: "basically works", 490 tg: &configpb.TestGroup{ 491 ResultSource: &configpb.TestGroup_ResultSource{ 492 ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{ 493 ResultstoreConfig: &configpb.ResultStoreConfig{ 494 Project: "my-project", 495 }, 496 }, 497 }, 498 }, 499 err: false, 500 }, 501 { 502 name: "valid query", 503 tg: &configpb.TestGroup{ 504 ResultSource: &configpb.TestGroup_ResultSource{ 505 ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{ 506 ResultstoreConfig: &configpb.ResultStoreConfig{ 507 Project: "my-project", 508 Query: `target:"my-job"`, 509 }, 510 }, 511 }, 512 }, 513 err: false, 514 }, 515 { 516 name: "invalid query", 517 tg: &configpb.TestGroup{ 518 ResultSource: &configpb.TestGroup_ResultSource{ 519 ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{ 520 ResultstoreConfig: &configpb.ResultStoreConfig{ 521 Project: "my-project", 522 Query: `label:foo bar`, 523 }, 524 }, 525 }, 526 }, 527 err: true, 528 }, 529 { 530 name: "query without project", 531 tg: &configpb.TestGroup{ 532 ResultSource: &configpb.TestGroup_ResultSource{ 533 ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{ 534 ResultstoreConfig: &configpb.ResultStoreConfig{ 535 Query: `target:"my-job"`, 536 }, 537 }, 538 }, 539 }, 540 err: true, 541 }, 542 { 543 name: "gcs_prefix and ResultStore defined", 544 tg: &configpb.TestGroup{ 545 GcsPrefix: "/my-bucket/logs", 546 ResultSource: &configpb.TestGroup_ResultSource{ 547 ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{ 548 ResultstoreConfig: &configpb.ResultStoreConfig{ 549 Project: "my-project", 550 }, 551 }, 552 }, 553 }, 554 err: true, 555 }, 556 { 557 name: "use_kubernetes_client and ResultStore defined", 558 tg: &configpb.TestGroup{ 559 UseKubernetesClient: true, 560 ResultSource: &configpb.TestGroup_ResultSource{ 561 ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{ 562 ResultstoreConfig: &configpb.ResultStoreConfig{ 563 Project: "my-project", 564 }, 565 }, 566 }, 567 }, 568 err: true, 569 }, 570 { 571 name: "other result source defined", 572 tg: &configpb.TestGroup{ 573 GcsPrefix: "/my-bucket/logs", 574 UseKubernetesClient: true, 575 }, 576 err: false, 577 }, 578 } 579 for _, test := range tests { 580 t.Run(test.name, func(t *testing.T) { 581 err := validateResultStoreSource(test.tg) 582 if err != nil && !test.err { 583 t.Errorf("validateResultStoreSource(%v) errored unexpectedly: %v", test.tg, err) 584 } else if err == nil && test.err { 585 t.Errorf("validateResultStoreSource(%v) did not error as expected", test.tg) 586 } 587 }) 588 } 589 } 590 591 func TestValidateGCSSource(t *testing.T) { 592 tests := []struct { 593 name string 594 tg *configpb.TestGroup 595 err bool 596 }{ 597 { 598 name: "nil test group", 599 tg: nil, 600 err: false, 601 }, 602 { 603 name: "empty test group", 604 tg: &configpb.TestGroup{}, 605 err: false, 606 }, 607 { 608 name: "empty GCS source", 609 tg: &configpb.TestGroup{ 610 ResultSource: &configpb.TestGroup_ResultSource{ 611 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 612 GcsConfig: &configpb.GCSConfig{}, 613 }, 614 }, 615 }, 616 err: true, 617 }, 618 { 619 name: "basically works", 620 tg: &configpb.TestGroup{ 621 ResultSource: &configpb.TestGroup_ResultSource{ 622 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 623 GcsConfig: &configpb.GCSConfig{ 624 GcsPrefix: "/my-bucket/logs", 625 }, 626 }, 627 }, 628 }, 629 err: false, 630 }, 631 { 632 name: "gcs_prefix and GCS config defined", 633 tg: &configpb.TestGroup{ 634 GcsPrefix: "/my-bucket/logs", 635 ResultSource: &configpb.TestGroup_ResultSource{ 636 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 637 GcsConfig: &configpb.GCSConfig{ 638 GcsPrefix: "/my-bucket/logs", 639 }, 640 }, 641 }, 642 }, 643 err: true, 644 }, 645 { 646 name: "use_kubernetes_client and GCS config defined", 647 tg: &configpb.TestGroup{ 648 UseKubernetesClient: true, 649 ResultSource: &configpb.TestGroup_ResultSource{ 650 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 651 GcsConfig: &configpb.GCSConfig{ 652 GcsPrefix: "/my-bucket/logs", 653 }, 654 }, 655 }, 656 }, 657 err: true, 658 }, 659 { 660 name: "other result source defined", 661 tg: &configpb.TestGroup{ 662 GcsPrefix: "/my-bucket/logs", 663 UseKubernetesClient: true, 664 }, 665 err: false, 666 }, 667 { 668 name: "GCS config with pubsub", 669 tg: &configpb.TestGroup{ 670 ResultSource: &configpb.TestGroup_ResultSource{ 671 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 672 GcsConfig: &configpb.GCSConfig{ 673 GcsPrefix: "/my-bucket/logs", 674 PubsubProject: "my-project", 675 PubsubSubscription: "my-gcs-notifications", 676 }, 677 }, 678 }, 679 }, 680 err: false, 681 }, 682 { 683 name: "GCS config with partial pubsub", 684 tg: &configpb.TestGroup{ 685 ResultSource: &configpb.TestGroup_ResultSource{ 686 ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{ 687 GcsConfig: &configpb.GCSConfig{ 688 GcsPrefix: "/my-bucket/logs", 689 PubsubProject: "my-project", 690 }, 691 }, 692 }, 693 }, 694 err: true, 695 }, 696 } 697 for _, test := range tests { 698 t.Run(test.name, func(t *testing.T) { 699 err := validateGCSSource(test.tg) 700 if err != nil && !test.err { 701 t.Errorf("validateGCSSource(%v) errored unexpectedly: %v", test.tg, err) 702 } else if err == nil && test.err { 703 t.Errorf("validateGCSSource(%v) did not error as expected", test.tg) 704 } 705 }) 706 } 707 } 708 709 func TestValidateTestGroup(t *testing.T) { 710 tests := []struct { 711 name string 712 testGroup *configpb.TestGroup 713 pass bool 714 }{ 715 { 716 name: "Nil TestGroup fails", 717 pass: false, 718 testGroup: nil, 719 }, 720 { 721 name: "Minimal config passes", 722 pass: true, 723 testGroup: &configpb.TestGroup{ 724 Name: "test_group", 725 DaysOfResults: 1, 726 GcsPrefix: "fake path", 727 NumColumnsRecent: 1, 728 }, 729 }, 730 { 731 name: "Must have days_of_results", 732 testGroup: &configpb.TestGroup{ 733 Name: "test_group", 734 GcsPrefix: "fake path", 735 NumColumnsRecent: 1, 736 }, 737 }, 738 { 739 name: "days_of_results must be positive", 740 testGroup: &configpb.TestGroup{ 741 Name: "test_group", 742 DaysOfResults: -1, 743 GcsPrefix: "fake path", 744 NumColumnsRecent: 1, 745 }, 746 }, 747 { 748 name: "Must have gcs_prefix", 749 testGroup: &configpb.TestGroup{ 750 Name: "test_group", 751 DaysOfResults: 1, 752 NumColumnsRecent: 1, 753 }, 754 }, 755 { 756 name: "Must have num_columns_recent", 757 testGroup: &configpb.TestGroup{ 758 Name: "test_group", 759 DaysOfResults: 1, 760 GcsPrefix: "fake path", 761 }, 762 }, 763 { 764 name: "num_columns_recent must be positive", 765 testGroup: &configpb.TestGroup{ 766 Name: "test_group", 767 DaysOfResults: 1, 768 GcsPrefix: "fake path", 769 NumColumnsRecent: -1, 770 }, 771 }, 772 { 773 name: "test_method_match_regex must compile", 774 testGroup: &configpb.TestGroup{ 775 Name: "test_group", 776 DaysOfResults: 1, 777 GcsPrefix: "fake path", 778 NumColumnsRecent: 1, 779 TestMethodMatchRegex: "[.*", 780 }, 781 }, 782 { 783 name: "Notifications must have a summary", 784 testGroup: &configpb.TestGroup{ 785 Name: "test_group", 786 DaysOfResults: 1, 787 GcsPrefix: "fake path", 788 NumColumnsRecent: 1, 789 Notifications: []*configpb.Notification{ 790 {}, 791 }, 792 }, 793 }, 794 { 795 name: "Test Annotations must have property_name", 796 testGroup: &configpb.TestGroup{ 797 Name: "test_group", 798 DaysOfResults: 1, 799 GcsPrefix: "fake path", 800 NumColumnsRecent: 1, 801 TestAnnotations: []*configpb.TestGroup_TestAnnotation{ 802 { 803 ShortText: "a", 804 }, 805 }, 806 }, 807 }, 808 { 809 name: "Test Annotation short_text has to be at least 1 character", 810 testGroup: &configpb.TestGroup{ 811 Name: "test_group", 812 DaysOfResults: 1, 813 GcsPrefix: "fake path", 814 NumColumnsRecent: 1, 815 TestAnnotations: []*configpb.TestGroup_TestAnnotation{ 816 { 817 ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{ 818 PropertyName: "something", 819 }, 820 ShortText: "", 821 }, 822 }, 823 }, 824 }, 825 { 826 name: "Test Annotation short_text has to be at most 5 characters", 827 testGroup: &configpb.TestGroup{ 828 Name: "test_group", 829 DaysOfResults: 1, 830 GcsPrefix: "fake path", 831 NumColumnsRecent: 1, 832 TestAnnotations: []*configpb.TestGroup_TestAnnotation{ 833 { 834 ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{ 835 PropertyName: "something", 836 }, 837 ShortText: "abcdef", 838 }, 839 }, 840 }, 841 }, 842 { 843 name: "fallback_grouping_configuration_value requires fallback_group = configuration_value", 844 testGroup: &configpb.TestGroup{ 845 Name: "test_group", 846 DaysOfResults: 1, 847 GcsPrefix: "fake path", 848 NumColumnsRecent: 1, 849 FallbackGroupingConfigurationValue: "something", 850 }, 851 }, 852 { 853 name: "fallback_grouping = configuration_value requires fallback_grouping_configuration_value", 854 testGroup: &configpb.TestGroup{ 855 Name: "test_group", 856 DaysOfResults: 1, 857 GcsPrefix: "fake path", 858 NumColumnsRecent: 1, 859 FallbackGrouping: configpb.TestGroup_FALLBACK_GROUPING_CONFIGURATION_VALUE, 860 }, 861 }, 862 { 863 name: "Complex config passes", 864 pass: true, 865 testGroup: &configpb.TestGroup{ 866 // Basic config 867 Name: "test_group", 868 DaysOfResults: 1, 869 GcsPrefix: "fake path", 870 NumColumnsRecent: 1, 871 // Regexes compile 872 TestMethodMatchRegex: "test.*", 873 // Simple notification 874 Notifications: []*configpb.Notification{ 875 { 876 Summary: "I'm a notification!", 877 }, 878 }, 879 // Fallback grouping based on a configuration value 880 FallbackGrouping: configpb.TestGroup_FALLBACK_GROUPING_CONFIGURATION_VALUE, 881 FallbackGroupingConfigurationValue: "something", 882 // Simple test annotation based on a property 883 TestAnnotations: []*configpb.TestGroup_TestAnnotation{ 884 { 885 ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{ 886 PropertyName: "something", 887 }, 888 ShortText: "abc", 889 }, 890 }, 891 }, 892 }, 893 { 894 name: "accept filled column headers", 895 pass: true, 896 testGroup: &configpb.TestGroup{ 897 Name: "test_group", 898 DaysOfResults: 1, 899 GcsPrefix: "fake path", 900 NumColumnsRecent: 1, 901 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 902 { 903 Label: "lab", 904 }, 905 { 906 Property: "prop", 907 }, 908 { 909 ConfigurationValue: "yay", 910 }, 911 }, 912 }, 913 }, 914 { 915 name: "reject column headers with label and configuration_value", 916 testGroup: &configpb.TestGroup{ 917 Name: "test_group", 918 DaysOfResults: 1, 919 GcsPrefix: "fake path", 920 NumColumnsRecent: 1, 921 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 922 { 923 Label: "labtoo", 924 ConfigurationValue: "foo", 925 }, 926 }, 927 }, 928 }, 929 { 930 name: "reject column headers with configuration_value and property", 931 testGroup: &configpb.TestGroup{ 932 Name: "test_group", 933 DaysOfResults: 1, 934 GcsPrefix: "fake path", 935 NumColumnsRecent: 1, 936 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 937 { 938 ConfigurationValue: "bar", 939 Property: "proptoo", 940 }, 941 }, 942 }, 943 }, 944 { 945 name: "reject column headers with label and property", 946 testGroup: &configpb.TestGroup{ 947 Name: "test_group", 948 DaysOfResults: 1, 949 GcsPrefix: "fake path", 950 NumColumnsRecent: 1, 951 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 952 { 953 Label: "labtoo", 954 Property: "proptoo", 955 }, 956 }, 957 }, 958 }, 959 { 960 name: "reject empty column headers", 961 testGroup: &configpb.TestGroup{ 962 Name: "test_group", 963 DaysOfResults: 1, 964 GcsPrefix: "fake path", 965 NumColumnsRecent: 1, 966 ColumnHeader: []*configpb.TestGroup_ColumnHeader{ 967 {}, 968 }, 969 }, 970 }, 971 { 972 name: "reject unformatted name format", 973 testGroup: &configpb.TestGroup{ 974 Name: "simple", 975 DaysOfResults: 1, 976 GcsPrefix: "fake path", 977 NumColumnsRecent: 1, 978 TestNameConfig: &configpb.TestNameConfig{ 979 NameFormat: "hello world", 980 }, 981 }, 982 }, 983 { 984 name: "accept complex and balanced name formats", 985 pass: true, 986 testGroup: &configpb.TestGroup{ 987 Name: "complex", 988 DaysOfResults: 1, 989 GcsPrefix: "fake path", 990 NumColumnsRecent: 1, 991 TestNameConfig: &configpb.TestNameConfig{ 992 NameFormat: "hello %s you are %s", 993 NameElements: []*configpb.TestNameConfig_NameElement{ 994 { 995 Labels: "world", 996 }, 997 { 998 Labels: "great", 999 }, 1000 }, 1001 }, 1002 }, 1003 }, 1004 { 1005 name: "reject unbalanced name formats", 1006 testGroup: &configpb.TestGroup{ 1007 Name: "bad", 1008 DaysOfResults: 1, 1009 GcsPrefix: "fake path", 1010 NumColumnsRecent: 1, 1011 TestNameConfig: &configpb.TestNameConfig{ 1012 NameFormat: "sorry %s but this is just too %s to tell you", 1013 NameElements: []*configpb.TestNameConfig_NameElement{ 1014 { 1015 Labels: "charlie", 1016 }, 1017 }, 1018 }, 1019 }, 1020 }, 1021 { 1022 name: "basic test metadata options", 1023 pass: true, 1024 testGroup: &configpb.TestGroup{ 1025 Name: "bad", 1026 DaysOfResults: 1, 1027 GcsPrefix: "fake path", 1028 NumColumnsRecent: 1, 1029 TestMetadataOptions: []*configpb.TestMetadataOptions{ 1030 { 1031 BugComponent: 1234, 1032 TestNameRegex: ".*stuff", 1033 }, 1034 }, 1035 }, 1036 }, 1037 { 1038 name: "test metadata options zero component allowed", 1039 pass: true, 1040 testGroup: &configpb.TestGroup{ 1041 Name: "bad", 1042 DaysOfResults: 1, 1043 GcsPrefix: "fake path", 1044 NumColumnsRecent: 1, 1045 TestMetadataOptions: []*configpb.TestMetadataOptions{ 1046 { 1047 BugComponent: 0, 1048 TestNameRegex: ".*stuff", 1049 }, 1050 }, 1051 }, 1052 }, 1053 { 1054 name: "test metadata options negative component allowed", 1055 pass: true, 1056 testGroup: &configpb.TestGroup{ 1057 Name: "bad", 1058 DaysOfResults: 1, 1059 GcsPrefix: "fake path", 1060 NumColumnsRecent: 1, 1061 TestMetadataOptions: []*configpb.TestMetadataOptions{ 1062 { 1063 BugComponent: -1, 1064 TestNameRegex: ".*stuff", 1065 }, 1066 }, 1067 }, 1068 }, 1069 { 1070 name: "invalid empty test metadata options", 1071 testGroup: &configpb.TestGroup{ 1072 Name: "bad", 1073 DaysOfResults: 1, 1074 GcsPrefix: "fake path", 1075 NumColumnsRecent: 1, 1076 TestMetadataOptions: []*configpb.TestMetadataOptions{ 1077 { 1078 BugComponent: 1234, 1079 }, 1080 }, 1081 }, 1082 }, 1083 { 1084 name: "invalid test name regex", 1085 testGroup: &configpb.TestGroup{ 1086 Name: "bad", 1087 DaysOfResults: 1, 1088 GcsPrefix: "fake path", 1089 NumColumnsRecent: 1, 1090 TestMetadataOptions: []*configpb.TestMetadataOptions{ 1091 { 1092 BugComponent: 1234, 1093 TestNameRegex: "?bad", 1094 }, 1095 }, 1096 }, 1097 }, 1098 { 1099 name: "invalid message regex", 1100 testGroup: &configpb.TestGroup{ 1101 Name: "bad", 1102 DaysOfResults: 1, 1103 GcsPrefix: "fake path", 1104 NumColumnsRecent: 1, 1105 TestMetadataOptions: []*configpb.TestMetadataOptions{ 1106 { 1107 BugComponent: 1234, 1108 MessageRegex: "?bad", 1109 }, 1110 }, 1111 }, 1112 }, 1113 } 1114 for _, test := range tests { 1115 t.Run(test.name, func(t *testing.T) { 1116 err := validateTestGroup(test.testGroup) 1117 pass := err == nil 1118 if test.pass != pass { 1119 t.Fatalf("test group config got pass = %v, want pass = %v: %v", pass, test.pass, err) 1120 } 1121 }) 1122 } 1123 } 1124 1125 func TestInvalidEmails(t *testing.T) { 1126 tests := []struct { 1127 name string 1128 addresses string 1129 pass bool 1130 }{ 1131 { 1132 name: "Addresses can't be blank", 1133 addresses: "", 1134 }, 1135 { 1136 name: "Comma-separated addresses can't be blank", 1137 addresses: ",", 1138 }, 1139 { 1140 name: "Comma-separated addresses still can't be blank", 1141 addresses: ",thing@email.com", 1142 }, 1143 { 1144 name: "no username", 1145 addresses: "@email.com", 1146 }, 1147 { 1148 name: "no domain name", 1149 addresses: "username", 1150 }, 1151 { 1152 name: "@ but no domain name", 1153 addresses: "username@", 1154 }, 1155 { 1156 name: "too many @'s", 1157 addresses: "hey@hello@greetings.com", 1158 }, 1159 { 1160 name: "Valid Address", 1161 addresses: "hey@greetings.com", 1162 pass: true, 1163 }, 1164 { 1165 name: "Multiple Valid Addresses", 1166 addresses: "hey@greetings.com,something@mail.com", 1167 pass: true, 1168 }, 1169 } 1170 for _, test := range tests { 1171 t.Run(test.name, func(t *testing.T) { 1172 err := validateEmails(test.addresses) 1173 pass := err == nil 1174 if test.pass != pass { 1175 t.Fatalf("addresses (%s) got pass = %v, want pass = %v: %v", test.addresses, pass, test.pass, err) 1176 } 1177 }) 1178 } 1179 } 1180 1181 func TestValidateDashboardTab(t *testing.T) { 1182 tests := []struct { 1183 name string 1184 tab *configpb.DashboardTab 1185 err bool 1186 }{ 1187 { 1188 name: "nil DashboardTab fails", 1189 tab: nil, 1190 err: true, 1191 }, 1192 { 1193 name: "tab, missing test group", 1194 tab: &configpb.DashboardTab{ 1195 Name: "tabby", 1196 }, 1197 err: true, 1198 }, 1199 { 1200 name: "tab, has test group", 1201 tab: &configpb.DashboardTab{ 1202 Name: "tabby", 1203 TestGroupName: "test_group_1", 1204 }, 1205 }, 1206 { 1207 name: "tabular names basically works", 1208 tab: &configpb.DashboardTab{ 1209 Name: "tabby", 1210 TestGroupName: "test_group_1", 1211 TabularNamesRegex: `(?P<hello>\d+).*(?P<hi>\d+)`, 1212 }, 1213 }, 1214 { 1215 name: "tabular names, invalid compile", 1216 tab: &configpb.DashboardTab{ 1217 Name: "tabby", 1218 TestGroupName: "test_group_1", 1219 TabularNamesRegex: `([1!]`, 1220 }, 1221 err: true, 1222 }, 1223 { 1224 name: "tabular names, 0 capture groups", 1225 tab: &configpb.DashboardTab{ 1226 Name: "tabby", 1227 TestGroupName: "test_group_1", 1228 TabularNamesRegex: `.*`, 1229 }, 1230 err: true, 1231 }, 1232 { 1233 name: "tabular names, unnamed capture groups", 1234 tab: &configpb.DashboardTab{ 1235 Name: "tabby", 1236 TestGroupName: "test_group_1", 1237 TabularNamesRegex: `(\d+).*(\d+)`, 1238 }, 1239 err: true, 1240 }, 1241 { 1242 name: "invalid max acceptable flakiness parameter", 1243 tab: &configpb.DashboardTab{ 1244 Name: "pug", 1245 TestGroupName: "test_group_2", 1246 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 1247 MaxAcceptableFlakiness: 101.5, 1248 }, 1249 }, 1250 err: true, 1251 }, 1252 { 1253 name: "tab, has testgroup, valid max acceptable flakiness parameter", 1254 tab: &configpb.DashboardTab{ 1255 Name: "pug", 1256 TestGroupName: "test_group_2", 1257 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 1258 MaxAcceptableFlakiness: 25.0, 1259 }, 1260 }, 1261 }, 1262 { 1263 name: "tab, has testgroup, valid max acceptable flakiness parameter, lower boundary", 1264 tab: &configpb.DashboardTab{ 1265 Name: "pug", 1266 TestGroupName: "test_group_2", 1267 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 1268 MaxAcceptableFlakiness: 0.0, 1269 }, 1270 }, 1271 }, 1272 { 1273 name: "tab, has testgroup, valid max acceptable flakiness parameter, upper boundary", 1274 tab: &configpb.DashboardTab{ 1275 Name: "pug", 1276 TestGroupName: "test_group_2", 1277 StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{ 1278 MaxAcceptableFlakiness: 100.0, 1279 }, 1280 }, 1281 }, 1282 } 1283 for _, test := range tests { 1284 t.Run(test.name, func(t *testing.T) { 1285 err := validateDashboardTab(test.tab) 1286 if err == nil && test.err { 1287 t.Fatalf("Did not get expected error") 1288 } 1289 if err != nil && !test.err { 1290 t.Fatalf("Got unexpected error: %v", err) 1291 } 1292 }) 1293 } 1294 } 1295 1296 func TestUpdate_Validate(t *testing.T) { 1297 tests := []struct { 1298 name string 1299 input *configpb.Configuration 1300 expectedErrs []error 1301 }{ 1302 { 1303 name: "Nil input; returns error", 1304 input: nil, 1305 expectedErrs: []error{errors.New("got an empty config.Configuration")}, 1306 }, 1307 { 1308 name: "Dashboard Only; returns error", 1309 input: &configpb.Configuration{ 1310 Dashboards: []*configpb.Dashboard{ 1311 { 1312 Name: "dash_1", 1313 }, 1314 }, 1315 }, 1316 expectedErrs: []error{ 1317 MissingFieldError{"TestGroups"}, 1318 }, 1319 }, 1320 { 1321 name: "Test Group Only; returns error", 1322 input: &configpb.Configuration{ 1323 TestGroups: []*configpb.TestGroup{ 1324 { 1325 Name: "test_group_1", 1326 }, 1327 }, 1328 }, 1329 expectedErrs: []error{ 1330 MissingFieldError{"Dashboards"}, 1331 }, 1332 }, 1333 { 1334 name: "Complete Minimal Config", 1335 input: &configpb.Configuration{ 1336 Dashboards: []*configpb.Dashboard{ 1337 { 1338 Name: "dash_1", 1339 DashboardTab: []*configpb.DashboardTab{ 1340 { 1341 Name: "tab_1", 1342 TestGroupName: "test_group_1", 1343 }, 1344 }, 1345 }, 1346 }, 1347 TestGroups: []*configpb.TestGroup{ 1348 { 1349 Name: "test_group_1", 1350 GcsPrefix: "fake GcsPrefix", 1351 DaysOfResults: 1, 1352 NumColumnsRecent: 1, 1353 }, 1354 }, 1355 }, 1356 }, 1357 { 1358 name: "Empty Dashboard; returns error", 1359 input: &configpb.Configuration{ 1360 Dashboards: []*configpb.Dashboard{ 1361 { 1362 Name: "dash_1", 1363 DashboardTab: []*configpb.DashboardTab{ 1364 { 1365 Name: "tab_1", 1366 TestGroupName: "test_group_1", 1367 }, 1368 }, 1369 }, 1370 { 1371 Name: "dash_2", 1372 }, 1373 }, 1374 TestGroups: []*configpb.TestGroup{ 1375 { 1376 Name: "test_group_1", 1377 GcsPrefix: "fake GcsPrefix", 1378 DaysOfResults: 1, 1379 NumColumnsRecent: 1, 1380 }, 1381 }, 1382 }, 1383 expectedErrs: []error{ 1384 ValidationError{"dash_2", "Dashboard", "contains no tabs"}, 1385 }, 1386 }, 1387 { 1388 name: "Dashboards and Dashboard Groups cannot share names.", 1389 input: &configpb.Configuration{ 1390 Dashboards: []*configpb.Dashboard{ 1391 { 1392 Name: "name_1", 1393 DashboardTab: []*configpb.DashboardTab{ 1394 { 1395 Name: "tab_1", 1396 TestGroupName: "test_group_1", 1397 }, 1398 }, 1399 }, 1400 }, 1401 DashboardGroups: []*configpb.DashboardGroup{ 1402 { 1403 Name: "name_1", 1404 }, 1405 }, 1406 TestGroups: []*configpb.TestGroup{ 1407 { 1408 Name: "test_group_1", 1409 GcsPrefix: "fake GcsPrefix", 1410 DaysOfResults: 1, 1411 NumColumnsRecent: 1, 1412 }, 1413 }, 1414 }, 1415 expectedErrs: []error{ 1416 DuplicateNameError{"name1", "Dashboard/DashboardGroup"}, 1417 }, 1418 }, 1419 { 1420 name: "Dashboard Tabs must reference an existing Test Group", 1421 input: &configpb.Configuration{ 1422 Dashboards: []*configpb.Dashboard{ 1423 { 1424 Name: "dash_1", 1425 DashboardTab: []*configpb.DashboardTab{ 1426 { 1427 Name: "tab_1", 1428 TestGroupName: "test_group_1", 1429 }, 1430 { 1431 Name: "tab_2", 1432 TestGroupName: "test_group_2", 1433 }, 1434 }, 1435 }, 1436 }, 1437 TestGroups: []*configpb.TestGroup{ 1438 { 1439 Name: "test_group_1", 1440 GcsPrefix: "fake GcsPrefix", 1441 DaysOfResults: 1, 1442 NumColumnsRecent: 1, 1443 }, 1444 }, 1445 }, 1446 expectedErrs: []error{ 1447 MissingEntityError{"test_group_2", "TestGroup"}, 1448 }, 1449 }, 1450 { 1451 name: "Test Groups must have an associated Dashboard Tab", 1452 input: &configpb.Configuration{ 1453 Dashboards: []*configpb.Dashboard{ 1454 { 1455 Name: "dash_1", 1456 DashboardTab: []*configpb.DashboardTab{ 1457 { 1458 Name: "tab_1", 1459 TestGroupName: "test_group_1", 1460 }, 1461 }, 1462 }, 1463 }, 1464 TestGroups: []*configpb.TestGroup{ 1465 { 1466 Name: "test_group_1", 1467 GcsPrefix: "fake GcsPrefix", 1468 DaysOfResults: 1, 1469 NumColumnsRecent: 1, 1470 }, 1471 { 1472 Name: "test_group_2", 1473 GcsPrefix: "fake GcsPrefix", 1474 DaysOfResults: 1, 1475 NumColumnsRecent: 1, 1476 }, 1477 }, 1478 }, 1479 expectedErrs: []error{ 1480 ValidationError{"test_group_2", "TestGroup", "Each Test Group must be referenced by at least 1 Dashboard Tab."}, 1481 }, 1482 }, 1483 { 1484 name: "Dashboard Groups must reference existing Dashboards", 1485 input: &configpb.Configuration{ 1486 Dashboards: []*configpb.Dashboard{ 1487 { 1488 Name: "dash_1", 1489 DashboardTab: []*configpb.DashboardTab{ 1490 { 1491 Name: "tab_1", 1492 TestGroupName: "test_group_1", 1493 }, 1494 }, 1495 }, 1496 }, 1497 TestGroups: []*configpb.TestGroup{ 1498 { 1499 Name: "test_group_1", 1500 GcsPrefix: "fake GcsPrefix", 1501 DaysOfResults: 1, 1502 NumColumnsRecent: 1, 1503 }, 1504 }, 1505 DashboardGroups: []*configpb.DashboardGroup{ 1506 { 1507 Name: "dash_group_1", 1508 DashboardNames: []string{"dash_1", "dash_2", "dash_3"}, 1509 }, 1510 }, 1511 }, 1512 expectedErrs: []error{ 1513 MissingEntityError{"dash_2", "Dashboard"}, 1514 MissingEntityError{"dash_3", "Dashboard"}, 1515 }, 1516 }, 1517 { 1518 name: "A Dashboard can belong to at most 1 Dashboard Group", 1519 input: &configpb.Configuration{ 1520 Dashboards: []*configpb.Dashboard{ 1521 { 1522 Name: "dash_1", 1523 DashboardTab: []*configpb.DashboardTab{ 1524 { 1525 Name: "tab_1", 1526 TestGroupName: "test_group_1", 1527 }, 1528 }, 1529 }, 1530 }, 1531 TestGroups: []*configpb.TestGroup{ 1532 { 1533 Name: "test_group_1", 1534 GcsPrefix: "fake GcsPrefix", 1535 DaysOfResults: 1, 1536 NumColumnsRecent: 1, 1537 }, 1538 }, 1539 DashboardGroups: []*configpb.DashboardGroup{ 1540 { 1541 Name: "dash_group_1", 1542 DashboardNames: []string{"dash_1"}, 1543 }, 1544 { 1545 Name: "dash_group_2", 1546 DashboardNames: []string{"dash_1"}, 1547 }, 1548 }, 1549 }, 1550 expectedErrs: []error{ 1551 ValidationError{"dash_1", "Dashboard", "A Dashboard cannot be in more than 1 Dashboard Group."}, 1552 }, 1553 }, 1554 } 1555 1556 for _, test := range tests { 1557 t.Run(test.name, func(t *testing.T) { 1558 err := Validate(test.input) 1559 if err != nil && len(test.expectedErrs) == 0 { 1560 t.Fatalf("Unexpected Error: %v", err) 1561 } 1562 1563 if len(test.expectedErrs) != 0 { 1564 if err == nil { 1565 t.Fatalf("Expected %v, but got no error", test.expectedErrs) 1566 } 1567 1568 if mErr, ok := err.(*multierror.Error); ok { 1569 if !reflect.DeepEqual(test.expectedErrs, mErr.Errors) { 1570 t.Fatalf("Expected %v, but got: %v", test.expectedErrs, mErr.Errors) 1571 } 1572 } else { 1573 t.Fatalf("Expected %v, but got: %v", test.expectedErrs, err) 1574 } 1575 } 1576 }) 1577 } 1578 }