sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/config/tide_test.go (about) 1 /* 2 Copyright 2017 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 "fmt" 21 "reflect" 22 "regexp" 23 "strings" 24 "testing" 25 26 "github.com/google/go-cmp/cmp" 27 "k8s.io/apimachinery/pkg/util/diff" 28 "k8s.io/apimachinery/pkg/util/sets" 29 utilpointer "k8s.io/utils/pointer" 30 "sigs.k8s.io/yaml" 31 32 "sigs.k8s.io/prow/pkg/git/types" 33 "sigs.k8s.io/prow/pkg/git/v2" 34 "sigs.k8s.io/prow/pkg/labels" 35 ) 36 37 var testQuery = TideQuery{ 38 Orgs: []string{"org"}, 39 Repos: []string{"k/k", "k/t-i"}, 40 ExcludedRepos: []string{"org/repo"}, 41 Labels: []string{labels.LGTM, labels.Approved, "this,or,that"}, 42 MissingLabels: []string{"foo"}, 43 Author: "batman", 44 Milestone: "milestone", 45 ReviewApprovedRequired: true, 46 } 47 48 var expectedQueryComponents = []string{ 49 "is:pr", 50 "state:open", 51 "archived:false", 52 "label:\"lgtm\"", 53 "label:\"approved\"", 54 "label:\"this\",\"or\",\"that\"", 55 "-label:\"foo\"", 56 "author:\"batman\"", 57 "milestone:\"milestone\"", 58 "review:approved", 59 } 60 61 func TestMarshalMergeMethod(t *testing.T) { 62 testCases := []struct { 63 testName string 64 tideConfig TideGitHubConfig 65 wantYaml string 66 }{ 67 { 68 testName: "Org-wide", 69 tideConfig: TideGitHubConfig{ 70 MergeType: map[string]TideOrgMergeType{ 71 "org1": { 72 MergeType: "squash", 73 }, 74 }, 75 }, 76 wantYaml: `context_options: {} 77 merge_method: 78 org1: squash 79 `, 80 }, 81 { 82 testName: "Empty org", 83 tideConfig: TideGitHubConfig{ 84 MergeType: map[string]TideOrgMergeType{ 85 "org1": {}, 86 }, 87 }, 88 wantYaml: `context_options: {} 89 merge_method: 90 org1: "" 91 `, 92 }, 93 { 94 testName: "Repo-wide", 95 tideConfig: TideGitHubConfig{ 96 MergeType: map[string]TideOrgMergeType{ 97 "org1": { 98 Repos: map[string]TideRepoMergeType{ 99 "repo1": { 100 MergeType: "rebase", 101 }, 102 }, 103 }, 104 }, 105 }, 106 wantYaml: `context_options: {} 107 merge_method: 108 org1: 109 repo1: rebase 110 `, 111 }, 112 { 113 testName: "Empty repo", 114 tideConfig: TideGitHubConfig{ 115 MergeType: map[string]TideOrgMergeType{ 116 "org1": { 117 Repos: map[string]TideRepoMergeType{ 118 "repo1": {}, 119 }, 120 }, 121 }, 122 }, 123 wantYaml: `context_options: {} 124 merge_method: 125 org1: 126 repo1: "" 127 `, 128 }, 129 { 130 testName: "Multiple branches", 131 tideConfig: TideGitHubConfig{ 132 MergeType: map[string]TideOrgMergeType{ 133 "org1": { 134 Repos: map[string]TideRepoMergeType{ 135 "repo1": { 136 Branches: map[string]TideBranchMergeType{ 137 "branch1": { 138 Regexpr: regexp.MustCompile("branch1"), 139 MergeType: types.MergeMerge, 140 }, 141 "branch2": { 142 Regexpr: regexp.MustCompile("branch2"), 143 MergeType: types.MergeSquash, 144 }, 145 }, 146 }, 147 }, 148 }, 149 }, 150 }, 151 wantYaml: `context_options: {} 152 merge_method: 153 org1: 154 repo1: 155 branch1: merge 156 branch2: squash 157 `, 158 }, 159 { 160 testName: "Complex", 161 tideConfig: TideGitHubConfig{ 162 MergeType: map[string]TideOrgMergeType{ 163 "org1": { 164 Repos: map[string]TideRepoMergeType{ 165 "repo1": { 166 Branches: map[string]TideBranchMergeType{ 167 "branch1": { 168 Regexpr: regexp.MustCompile("branch1"), 169 MergeType: types.MergeMerge, 170 }, 171 "branch2": { 172 Regexpr: regexp.MustCompile("branch2"), 173 MergeType: types.MergeSquash, 174 }, 175 }, 176 }, 177 }, 178 }, 179 "org2/repo1@master": { 180 MergeType: "squash", 181 }, 182 "org2": { 183 Repos: map[string]TideRepoMergeType{ 184 "repo2": { 185 MergeType: "merge", 186 }, 187 }, 188 }, 189 "org3": { 190 MergeType: "rebase", 191 }, 192 }, 193 }, 194 wantYaml: `context_options: {} 195 merge_method: 196 org1: 197 repo1: 198 branch1: merge 199 branch2: squash 200 org2: 201 repo2: merge 202 org2/repo1@master: squash 203 org3: rebase 204 `, 205 }, 206 } 207 for _, testCase := range testCases { 208 t.Run(testCase.testName, func(t *testing.T) { 209 yaml, err := yaml.Marshal(testCase.tideConfig) 210 if err != nil { 211 t.Errorf("unmarshal error: %v", err) 212 } 213 if diff := cmp.Diff(testCase.wantYaml, string(yaml)); diff != "" { 214 t.Errorf("unexpected yaml: %s", diff) 215 } 216 }) 217 } 218 } 219 220 func TestUnmarshalMergeMethod(t *testing.T) { 221 testCases := []struct { 222 testName string 223 yamlConfig string 224 wantConfig Config 225 }{ 226 { 227 testName: "No merge config", 228 yamlConfig: `tide:`, 229 wantConfig: Config{ 230 ProwConfig: ProwConfig{ 231 Tide: Tide{}, 232 }, 233 }, 234 }, 235 { 236 testName: "Mix OrgRepo and branches config", 237 yamlConfig: ` 238 tide: 239 merge_method: 240 org1: 241 repo1: 242 branch1: merge 243 org2/repo1: rebase`, 244 wantConfig: Config{ 245 ProwConfig: ProwConfig{ 246 Tide: Tide{ 247 TideGitHubConfig: TideGitHubConfig{ 248 MergeType: map[string]TideOrgMergeType{ 249 "org1": { 250 Repos: map[string]TideRepoMergeType{ 251 "repo1": { 252 Branches: map[string]TideBranchMergeType{ 253 "branch1": { 254 MergeType: types.MergeMerge, 255 }, 256 }, 257 }, 258 }, 259 }, 260 "org2/repo1": { 261 MergeType: types.MergeRebase, 262 }, 263 }, 264 }, 265 }, 266 }, 267 }, 268 }, 269 { 270 testName: "The same repo-wide and per branch config", 271 yamlConfig: ` 272 tide: 273 merge_method: 274 org1: 275 repo1: 276 branch1: merge 277 org1/repo1: rebase`, 278 wantConfig: Config{ 279 ProwConfig: ProwConfig{ 280 Tide: Tide{ 281 TideGitHubConfig: TideGitHubConfig{ 282 MergeType: map[string]TideOrgMergeType{ 283 "org1": { 284 Repos: map[string]TideRepoMergeType{ 285 "repo1": { 286 Branches: map[string]TideBranchMergeType{ 287 "branch1": { 288 MergeType: types.MergeMerge, 289 }, 290 }, 291 }, 292 }, 293 }, 294 "org1/repo1": { 295 MergeType: types.MergeRebase, 296 }, 297 }, 298 }, 299 }, 300 }, 301 }, 302 }, 303 { 304 testName: "Legacy repo only config", 305 yamlConfig: ` 306 tide: 307 merge_method: 308 org1/repo1: merge`, 309 wantConfig: Config{ 310 ProwConfig: ProwConfig{ 311 Tide: Tide{ 312 TideGitHubConfig: TideGitHubConfig{ 313 MergeType: map[string]TideOrgMergeType{ 314 "org1/repo1": { 315 MergeType: types.MergeMerge, 316 }, 317 }, 318 }, 319 }, 320 }, 321 }, 322 }, 323 { 324 testName: "Repo only config", 325 yamlConfig: ` 326 tide: 327 merge_method: 328 org1: 329 repo1: merge`, 330 wantConfig: Config{ 331 ProwConfig: ProwConfig{ 332 Tide: Tide{ 333 TideGitHubConfig: TideGitHubConfig{ 334 MergeType: map[string]TideOrgMergeType{ 335 "org1": { 336 Repos: map[string]TideRepoMergeType{ 337 "repo1": { 338 MergeType: types.MergeMerge, 339 }, 340 }, 341 }, 342 }, 343 }, 344 }, 345 }, 346 }, 347 }, 348 { 349 testName: "Org only config", 350 yamlConfig: ` 351 tide: 352 merge_method: 353 org1: rebase`, 354 wantConfig: Config{ 355 ProwConfig: ProwConfig{ 356 Tide: Tide{ 357 TideGitHubConfig: TideGitHubConfig{ 358 MergeType: map[string]TideOrgMergeType{ 359 "org1": { 360 MergeType: types.MergeRebase, 361 }, 362 }, 363 }, 364 }, 365 }, 366 }, 367 }, 368 { 369 testName: "Branches only config", 370 yamlConfig: ` 371 tide: 372 merge_method: 373 org1: 374 repo1: 375 branch1: merge`, 376 wantConfig: Config{ 377 ProwConfig: ProwConfig{ 378 Tide: Tide{ 379 TideGitHubConfig: TideGitHubConfig{ 380 MergeType: map[string]TideOrgMergeType{ 381 "org1": { 382 Repos: map[string]TideRepoMergeType{ 383 "repo1": { 384 Branches: map[string]TideBranchMergeType{ 385 "branch1": { 386 MergeType: types.MergeMerge, 387 }, 388 }, 389 }, 390 }, 391 }, 392 }, 393 }, 394 }, 395 }, 396 }, 397 }, 398 { 399 testName: "Branches config: wildcard on branch and repo", 400 yamlConfig: ` 401 tide: 402 merge_method: 403 org1: 404 ".*": 405 ".+": merge`, 406 wantConfig: Config{ 407 ProwConfig: ProwConfig{ 408 Tide: Tide{ 409 TideGitHubConfig: TideGitHubConfig{ 410 MergeType: map[string]TideOrgMergeType{ 411 "org1": { 412 Repos: map[string]TideRepoMergeType{ 413 ".*": { 414 Branches: map[string]TideBranchMergeType{ 415 ".+": { 416 MergeType: types.MergeMerge, 417 }, 418 }, 419 }, 420 }, 421 }, 422 }, 423 }, 424 }, 425 }, 426 }, 427 }, 428 { 429 testName: "Branches config: no repos at all", 430 yamlConfig: ` 431 tide: 432 merge_method: 433 ".*":`, 434 wantConfig: Config{ 435 ProwConfig: ProwConfig{ 436 Tide: Tide{ 437 TideGitHubConfig: TideGitHubConfig{ 438 MergeType: map[string]TideOrgMergeType{ 439 ".*": {}, 440 }, 441 }, 442 }, 443 }, 444 }, 445 }, 446 } 447 for _, testCase := range testCases { 448 t.Run(testCase.testName, func(t *testing.T) { 449 var config Config 450 if err := yaml.Unmarshal([]byte(testCase.yamlConfig), &config); err != nil { 451 t.Fatalf("unmarshal error: %v", err) 452 } 453 if diff := cmp.Diff(&testCase.wantConfig, &config); diff != "" { 454 t.Errorf("merge method configurations differ: %s", diff) 455 } 456 }) 457 } 458 } 459 460 func TestTideQuery(t *testing.T) { 461 q := " " + testQuery.Query() + " " 462 checkTok := checkTok(t, q) 463 464 checkTok("org:\"org\"") 465 checkTok("repo:\"k/k\"") 466 checkTok("repo:\"k/t-i\"") 467 checkTok("-repo:\"org/repo\"") 468 for _, expectedComponent := range expectedQueryComponents { 469 checkTok(expectedComponent) 470 } 471 472 elements := strings.Fields(q) 473 alreadySeen := sets.Set[string]{} 474 for _, element := range elements { 475 if alreadySeen.Has(element) { 476 t.Errorf("element %q was multiple times in the query string", element) 477 } 478 alreadySeen.Insert(element) 479 } 480 } 481 482 func checkTok(t *testing.T, q string) func(tok string) { 483 return func(tok string) { 484 t.Run("Query string contains "+tok, func(t *testing.T) { 485 if !strings.Contains(q, " "+tok+" ") { 486 t.Errorf("Expected query to contain \"%s\", got \"%s\"", tok, q) 487 } 488 }) 489 } 490 } 491 492 func TestOrgQueries(t *testing.T) { 493 queries := testQuery.OrgQueries() 494 if n := len(queries); n != 2 { 495 t.Errorf("expected exactly two queries, got %d", n) 496 } 497 if queries["org"] == "" { 498 t.Error("no query for org org found") 499 } 500 if queries["k"] == "" { 501 t.Error("no query for org k found") 502 } 503 504 for org, query := range queries { 505 t.Run(org, func(t *testing.T) { 506 checkTok := checkTok(t, " "+query+" ") 507 t.Logf("query: %s", query) 508 509 for _, expectedComponent := range expectedQueryComponents { 510 checkTok(expectedComponent) 511 } 512 513 elements := strings.Fields(query) 514 alreadySeen := sets.Set[string]{} 515 for _, element := range elements { 516 if alreadySeen.Has(element) { 517 t.Errorf("element %q was multiple times in the query string", element) 518 } 519 alreadySeen.Insert(element) 520 } 521 522 if org == "org" { 523 checkTok(`org:"org"`) 524 checkTok(`-repo:"org/repo"`) 525 } 526 527 if org == "k" { 528 for _, repo := range testQuery.Repos { 529 checkTok(fmt.Sprintf(`repo:"%s"`, repo)) 530 } 531 } 532 }) 533 } 534 } 535 536 func TestOrgExceptionsAndRepos(t *testing.T) { 537 queries := TideQueries{ 538 { 539 Orgs: []string{"k8s"}, 540 ExcludedRepos: []string{"k8s/k8s"}, 541 }, 542 { 543 Orgs: []string{"kuber"}, 544 Repos: []string{"foo/bar", "baz/bar"}, 545 ExcludedRepos: []string{"kuber/netes"}, 546 }, 547 { 548 Orgs: []string{"k8s"}, 549 ExcludedRepos: []string{"k8s/k8s", "k8s/t-i"}, 550 }, 551 { 552 Orgs: []string{"org", "org2"}, 553 ExcludedRepos: []string{"org2/repo", "org2/repo2", "org2/repo3"}, 554 }, 555 { 556 Orgs: []string{"foo"}, 557 Repos: []string{"org2/repo3"}, 558 }, 559 } 560 561 expectedOrgs := map[string]sets.Set[string]{ 562 "foo": sets.New[string](), 563 "k8s": sets.New[string]("k8s/k8s"), 564 "kuber": sets.New[string]("kuber/netes"), 565 "org": sets.New[string](), 566 "org2": sets.New[string]("org2/repo", "org2/repo2"), 567 } 568 expectedRepos := sets.New[string]("foo/bar", "baz/bar", "org2/repo3") 569 570 orgs, repos := queries.OrgExceptionsAndRepos() 571 if !reflect.DeepEqual(orgs, expectedOrgs) { 572 t.Errorf("Expected org map %v, but got %v.", expectedOrgs, orgs) 573 } 574 if !repos.Equal(expectedRepos) { 575 t.Errorf("Expected repo set %v, but got %v.", expectedRepos, repos) 576 } 577 } 578 579 func TestMergeMethod(t *testing.T) { 580 ti := &Tide{ 581 TideGitHubConfig: TideGitHubConfig{ 582 MergeType: map[string]TideOrgMergeType{ 583 "kubernetes/kops": {MergeType: types.MergeRebase}, 584 "kubernetes-helm": {MergeType: types.MergeSquash}, 585 "kubernetes-helm/chartmuseum": {MergeType: types.MergeMerge}, 586 }, 587 }, 588 } 589 590 var testcases = []struct { 591 org string 592 repo string 593 expected types.PullRequestMergeType 594 }{ 595 { 596 "kubernetes", 597 "kubernetes", 598 types.MergeMerge, 599 }, 600 { 601 "kubernetes", 602 "kops", 603 types.MergeRebase, 604 }, 605 { 606 "kubernetes-helm", 607 "monocular", 608 types.MergeSquash, 609 }, 610 { 611 "kubernetes-helm", 612 "chartmuseum", 613 types.MergeMerge, 614 }, 615 } 616 617 for _, test := range testcases { 618 actual := ti.MergeMethod(OrgRepo{Org: test.org, Repo: test.repo}) 619 if actual != test.expected { 620 t.Errorf("Expected merge method %q but got %q for %s/%s", test.expected, actual, test.org, test.repo) 621 } 622 } 623 } 624 625 func TestOrgRepoMatchMergeMethod(t *testing.T) { 626 var testCases = []struct { 627 name string 628 config Tide 629 org string 630 repo string 631 branch string 632 expected types.PullRequestMergeType 633 }{ 634 // Edge cases 635 { 636 name: "No input at all", 637 config: Tide{ 638 TideGitHubConfig: TideGitHubConfig{ 639 MergeType: map[string]TideOrgMergeType{ 640 "kubernetes": { 641 Repos: map[string]TideRepoMergeType{ 642 "test-infra": { 643 Branches: map[string]TideBranchMergeType{ 644 "master": { 645 Regexpr: regexp.MustCompile("master"), 646 MergeType: types.MergeRebase, 647 }, 648 }, 649 }, 650 }, 651 }, 652 }, 653 }, 654 }, 655 expected: types.MergeMerge, 656 }, 657 { 658 name: "Empty tide config", 659 config: Tide{}, 660 org: "kubernetes", 661 repo: "test-infra", 662 branch: "master", 663 expected: types.MergeMerge, 664 }, 665 { 666 name: "Empty tide config and no input", 667 config: Tide{}, 668 expected: types.MergeMerge, 669 }, 670 // Shorthands 671 { 672 name: "org shorthand: match", 673 config: Tide{ 674 TideGitHubConfig: TideGitHubConfig{ 675 MergeType: map[string]TideOrgMergeType{ 676 "kubernetes-helm": {MergeType: types.MergeSquash}, 677 }, 678 }, 679 }, 680 org: "kubernetes-helm", 681 expected: types.MergeSquash, 682 }, 683 { 684 name: "org shorthand: no match", 685 config: Tide{ 686 TideGitHubConfig: TideGitHubConfig{ 687 MergeType: map[string]TideOrgMergeType{ 688 "kubernetes-helm": {MergeType: types.MergeSquash}, 689 }, 690 }, 691 }, 692 org: "kubernetes", 693 expected: types.MergeMerge, 694 }, 695 { 696 name: "org shorthand: no org provided", 697 config: Tide{ 698 TideGitHubConfig: TideGitHubConfig{ 699 MergeType: map[string]TideOrgMergeType{ 700 "kubernetes": {MergeType: types.MergeRebase}, 701 }, 702 }, 703 }, 704 expected: types.MergeMerge, 705 }, 706 { 707 name: "org shorthand: neither repo nor branch matches", 708 config: Tide{ 709 TideGitHubConfig: TideGitHubConfig{ 710 MergeType: map[string]TideOrgMergeType{ 711 "kubernetes": {MergeType: types.MergeRebase}, 712 }, 713 }, 714 }, 715 org: "kubernetes", 716 repo: "test-infra", 717 branch: "dev", 718 expected: types.MergeRebase, 719 }, 720 { 721 name: "org/repo shorthand: match", 722 config: Tide{ 723 TideGitHubConfig: TideGitHubConfig{ 724 MergeType: map[string]TideOrgMergeType{ 725 "kubernetes-helm/chartmuseum": {MergeType: types.MergeSquash}, 726 }, 727 }, 728 }, 729 org: "kubernetes-helm", 730 repo: "chartmuseum", 731 expected: types.MergeSquash, 732 }, 733 { 734 name: "org/repo shorthand: no match", 735 config: Tide{ 736 TideGitHubConfig: TideGitHubConfig{ 737 MergeType: map[string]TideOrgMergeType{ 738 "kubernetes-helm/chartmuseum": {MergeType: types.MergeSquash}, 739 }, 740 }, 741 }, 742 org: "kubernetes-helm", 743 repo: "test-infra", 744 expected: types.MergeMerge, 745 }, 746 { 747 name: "org/repo shorthand: no repo provided", 748 config: Tide{ 749 TideGitHubConfig: TideGitHubConfig{ 750 MergeType: map[string]TideOrgMergeType{ 751 "kubernetes-helm/chartmuseum": {MergeType: types.MergeSquash}, 752 }, 753 }, 754 }, 755 org: "kubernetes-helm", 756 expected: types.MergeMerge, 757 }, 758 { 759 name: "org/repo shorthand: org only match", 760 config: Tide{ 761 TideGitHubConfig: TideGitHubConfig{ 762 MergeType: map[string]TideOrgMergeType{ 763 "kubernetes-helm": {MergeType: types.MergeSquash}, 764 }, 765 }, 766 }, 767 org: "kubernetes-helm", 768 repo: "chartmuseum", 769 expected: types.MergeSquash, 770 }, 771 { 772 name: "org/repo shorthand: fallback to org/repo when branch doesn't match", 773 config: Tide{ 774 TideGitHubConfig: TideGitHubConfig{ 775 MergeType: map[string]TideOrgMergeType{ 776 "kubernetes-helm/chartmuseum": {MergeType: types.MergeSquash}, 777 }, 778 }, 779 }, 780 org: "kubernetes-helm", 781 repo: "chartmuseum", 782 branch: "master", 783 expected: types.MergeSquash, 784 }, 785 { 786 name: "org/repo@branch shorthand: match", 787 config: Tide{ 788 TideGitHubConfig: TideGitHubConfig{ 789 MergeType: map[string]TideOrgMergeType{ 790 "kubernetes/kops@main": {MergeType: types.MergeRebase}, 791 }, 792 }, 793 }, 794 org: "kubernetes", 795 repo: "kops", 796 branch: "main", 797 expected: types.MergeRebase, 798 }, 799 { 800 name: "org/repo@branch shorthand: no match", 801 config: Tide{ 802 TideGitHubConfig: TideGitHubConfig{ 803 MergeType: map[string]TideOrgMergeType{ 804 "kubernetes/kops@main": {MergeType: types.MergeRebase}, 805 }, 806 }, 807 }, 808 org: "kubernetes", 809 repo: "kops", 810 branch: "master", 811 expected: types.MergeMerge, 812 }, 813 { 814 name: "org/repo@branch shorthand: no branch provided", 815 config: Tide{ 816 TideGitHubConfig: TideGitHubConfig{ 817 MergeType: map[string]TideOrgMergeType{ 818 "kubernetes/kops@main": {MergeType: types.MergeRebase}, 819 }, 820 }, 821 }, 822 org: "kubernetes", 823 repo: "kops", 824 expected: types.MergeMerge, 825 }, 826 // Repo-wide config 827 { 828 name: "Repo-wide config: match", 829 config: Tide{ 830 TideGitHubConfig: TideGitHubConfig{ 831 MergeType: map[string]TideOrgMergeType{ 832 "kubernetes": { 833 Repos: map[string]TideRepoMergeType{ 834 "kubernetes": {MergeType: types.MergeIfNecessary}, 835 }, 836 }, 837 }, 838 }, 839 }, 840 org: "kubernetes", 841 repo: "kubernetes", 842 expected: types.MergeIfNecessary, 843 }, 844 { 845 name: "Repo-wide config: no match", 846 config: Tide{ 847 TideGitHubConfig: TideGitHubConfig{ 848 MergeType: map[string]TideOrgMergeType{ 849 "kubernetes": { 850 Repos: map[string]TideRepoMergeType{ 851 "kubernetes": {MergeType: types.MergeIfNecessary}, 852 }, 853 }, 854 }, 855 }, 856 }, 857 org: "kubernetes", 858 repo: "test-infra", 859 expected: types.MergeMerge, 860 }, 861 { 862 name: "Repo-wide config: match using '*'", 863 config: Tide{ 864 TideGitHubConfig: TideGitHubConfig{ 865 MergeType: map[string]TideOrgMergeType{ 866 "kubernetes": { 867 Repos: map[string]TideRepoMergeType{ 868 "*": {MergeType: types.MergeIfNecessary}, 869 "kubernetes": {MergeType: types.MergeSquash}, 870 }, 871 }, 872 }, 873 }, 874 }, 875 org: "kubernetes", 876 repo: "test-infra", 877 expected: types.MergeIfNecessary, 878 }, 879 // Branch level config 880 { 881 name: "Branch level config: no match", 882 config: Tide{ 883 TideGitHubConfig: TideGitHubConfig{ 884 MergeType: map[string]TideOrgMergeType{ 885 "kubernetes": { 886 Repos: map[string]TideRepoMergeType{ 887 "test-infra": { 888 Branches: map[string]TideBranchMergeType{ 889 "master": { 890 Regexpr: regexp.MustCompile("master"), 891 MergeType: types.MergeRebase, 892 }, 893 }, 894 }, 895 }, 896 }, 897 }, 898 }, 899 }, 900 org: "kubernetes", 901 repo: "test-infra", 902 branch: "main", 903 expected: types.MergeMerge, 904 }, 905 { 906 name: "Branch level config: match no regex", 907 config: Tide{ 908 TideGitHubConfig: TideGitHubConfig{ 909 MergeType: map[string]TideOrgMergeType{ 910 "kubernetes": { 911 Repos: map[string]TideRepoMergeType{ 912 "test-infra": { 913 Branches: map[string]TideBranchMergeType{ 914 "master": { 915 Regexpr: regexp.MustCompile("master"), 916 MergeType: types.MergeRebase, 917 }, 918 }, 919 }, 920 }, 921 }, 922 }, 923 }, 924 }, 925 org: "kubernetes", 926 repo: "test-infra", 927 branch: "master", 928 expected: types.MergeRebase, 929 }, 930 { 931 name: "Branch level config: match regex", 932 config: Tide{ 933 TideGitHubConfig: TideGitHubConfig{ 934 MergeType: map[string]TideOrgMergeType{ 935 "kubernetes": { 936 Repos: map[string]TideRepoMergeType{ 937 "test-infra": { 938 Branches: map[string]TideBranchMergeType{ 939 `release-\d+(.\d+)?`: { 940 Regexpr: regexp.MustCompile(`release-\d+(.\d+)?`), 941 MergeType: types.MergeSquash, 942 }, 943 }, 944 }, 945 }, 946 }, 947 }, 948 }, 949 }, 950 org: "kubernetes", 951 repo: "test-infra", 952 branch: "release-0.2", 953 expected: types.MergeSquash, 954 }, 955 { 956 name: "Branch level config: multiple regex matches, pick the first one", 957 config: Tide{ 958 TideGitHubConfig: TideGitHubConfig{ 959 MergeType: map[string]TideOrgMergeType{ 960 "kubernetes": { 961 Repos: map[string]TideRepoMergeType{ 962 "test-infra": { 963 Branches: map[string]TideBranchMergeType{ 964 `ma.*`: { 965 Regexpr: regexp.MustCompile(`ma.*`), 966 MergeType: types.MergeSquash, 967 }, 968 `mast.*`: { 969 Regexpr: regexp.MustCompile(`ma.*`), 970 MergeType: types.MergeIfNecessary, 971 }, 972 }, 973 }, 974 }, 975 }, 976 }, 977 }, 978 }, 979 org: "kubernetes", 980 repo: "test-infra", 981 branch: "master", 982 expected: types.MergeSquash, 983 }, 984 { 985 name: "Branch level config: match '*' wildcard at repository level", 986 config: Tide{ 987 TideGitHubConfig: TideGitHubConfig{ 988 MergeType: map[string]TideOrgMergeType{ 989 "golang": { 990 Repos: map[string]TideRepoMergeType{ 991 "*": { 992 Branches: map[string]TideBranchMergeType{ 993 "main": { 994 Regexpr: regexp.MustCompile("main"), 995 MergeType: types.MergeIfNecessary, 996 }, 997 }, 998 }, 999 }, 1000 }, 1001 }, 1002 }, 1003 }, 1004 org: "golang", 1005 repo: "docs", 1006 branch: "main", 1007 expected: types.MergeIfNecessary, 1008 }, 1009 // Precedences 1010 { 1011 name: "Precedence: org/repo@branch shorthand over branch level config", 1012 config: Tide{ 1013 TideGitHubConfig: TideGitHubConfig{ 1014 MergeType: map[string]TideOrgMergeType{ 1015 "kubernetes/kops@main": {MergeType: types.MergeSquash}, 1016 "kubernetes": { 1017 Repos: map[string]TideRepoMergeType{ 1018 "kops": { 1019 Branches: map[string]TideBranchMergeType{ 1020 "main": { 1021 Regexpr: regexp.MustCompile("main"), 1022 MergeType: types.MergeIfNecessary, 1023 }, 1024 }, 1025 }, 1026 }, 1027 }, 1028 }, 1029 }, 1030 }, 1031 org: "kubernetes", 1032 repo: "kops", 1033 branch: "main", 1034 expected: types.MergeSquash, 1035 }, 1036 { 1037 name: "Precedence: branch level config over org/repo shorthand", 1038 config: Tide{ 1039 TideGitHubConfig: TideGitHubConfig{ 1040 MergeType: map[string]TideOrgMergeType{ 1041 "kubernetes/kops": {MergeType: types.MergeSquash}, 1042 "kubernetes": { 1043 Repos: map[string]TideRepoMergeType{ 1044 "kops": { 1045 Branches: map[string]TideBranchMergeType{ 1046 "main": { 1047 Regexpr: regexp.MustCompile("main"), 1048 MergeType: types.MergeIfNecessary, 1049 }, 1050 }, 1051 }, 1052 }, 1053 }, 1054 }, 1055 }, 1056 }, 1057 org: "kubernetes", 1058 repo: "kops", 1059 branch: "main", 1060 expected: types.MergeIfNecessary, 1061 }, 1062 { 1063 name: "Precedence: org/repo shorthand over repo-wide config", 1064 config: Tide{ 1065 TideGitHubConfig: TideGitHubConfig{ 1066 MergeType: map[string]TideOrgMergeType{ 1067 "kubernetes/kops": {MergeType: types.MergeSquash}, 1068 "kubernetes": { 1069 Repos: map[string]TideRepoMergeType{ 1070 "kops": {MergeType: types.MergeRebase}, 1071 }, 1072 }, 1073 }, 1074 }, 1075 }, 1076 org: "kubernetes", 1077 repo: "kops", 1078 expected: types.MergeSquash, 1079 }, 1080 { 1081 name: "Precedence: org/repo shorthand over org config", 1082 config: Tide{ 1083 TideGitHubConfig: TideGitHubConfig{ 1084 MergeType: map[string]TideOrgMergeType{ 1085 "kubernetes/kops": {MergeType: types.MergeSquash}, 1086 "kubernetes": {MergeType: types.MergeIfNecessary}, 1087 }, 1088 }, 1089 }, 1090 org: "kubernetes", 1091 repo: "kops", 1092 expected: types.MergeSquash, 1093 }, 1094 } 1095 for _, test := range testCases { 1096 test := test 1097 t.Run(test.name, func(t *testing.T) { 1098 actual := test.config.OrgRepoBranchMergeMethod(OrgRepo{Org: test.org, Repo: test.repo}, test.branch) 1099 if actual != test.expected { 1100 t.Errorf("Expected merge method %q but got %q for org: %q, repo: %q, branch: %q", 1101 test.expected, actual, test.org, test.repo, test.branch) 1102 } 1103 }) 1104 } 1105 } 1106 func TestMergeTemplate(t *testing.T) { 1107 ti := &Tide{ 1108 TideGitHubConfig: TideGitHubConfig{ 1109 MergeTemplate: map[string]TideMergeCommitTemplate{ 1110 "kubernetes/kops": { 1111 TitleTemplate: "", 1112 BodyTemplate: "", 1113 }, 1114 "kubernetes-helm": { 1115 TitleTemplate: "{{ .Title }}", 1116 BodyTemplate: "{{ .Body }}", 1117 }, 1118 }, 1119 }, 1120 } 1121 1122 var testcases = []struct { 1123 org string 1124 repo string 1125 expected TideMergeCommitTemplate 1126 }{ 1127 { 1128 org: "kubernetes", 1129 repo: "kubernetes", 1130 expected: TideMergeCommitTemplate{}, 1131 }, 1132 { 1133 org: "kubernetes", 1134 repo: "kops", 1135 expected: TideMergeCommitTemplate{ 1136 TitleTemplate: "", 1137 BodyTemplate: "", 1138 }, 1139 }, 1140 { 1141 org: "kubernetes-helm", 1142 repo: "monocular", 1143 expected: TideMergeCommitTemplate{ 1144 TitleTemplate: "{{ .Title }}", 1145 BodyTemplate: "{{ .Body }}", 1146 }, 1147 }, 1148 } 1149 1150 for _, test := range testcases { 1151 actual := ti.MergeCommitTemplate(OrgRepo{Org: test.org, Repo: test.repo}) 1152 1153 if actual.TitleTemplate != test.expected.TitleTemplate || actual.BodyTemplate != test.expected.BodyTemplate { 1154 t.Errorf("Expected title \"%v\", body \"%v\", but got title \"%v\", body \"%v\" for %v/%v", test.expected.TitleTemplate, test.expected.BodyTemplate, actual.TitleTemplate, actual.BodyTemplate, test.org, test.repo) 1155 } 1156 } 1157 } 1158 1159 func TestParseTideContextPolicyOptions(t *testing.T) { 1160 yes := true 1161 no := false 1162 org, repo, branch := "org", "repo", "branch" 1163 testCases := []struct { 1164 name string 1165 config TideContextPolicyOptions 1166 expected TideContextPolicy 1167 }{ 1168 { 1169 name: "empty", 1170 }, 1171 { 1172 name: "global config", 1173 config: TideContextPolicyOptions{ 1174 TideContextPolicy: TideContextPolicy{ 1175 FromBranchProtection: &yes, 1176 SkipUnknownContexts: &yes, 1177 RequiredContexts: []string{"r1"}, 1178 OptionalContexts: []string{"o1"}, 1179 }, 1180 }, 1181 expected: TideContextPolicy{ 1182 SkipUnknownContexts: &yes, 1183 RequiredContexts: []string{"r1"}, 1184 OptionalContexts: []string{"o1"}, 1185 FromBranchProtection: &yes, 1186 }, 1187 }, 1188 { 1189 name: "org config", 1190 config: TideContextPolicyOptions{ 1191 TideContextPolicy: TideContextPolicy{ 1192 RequiredContexts: []string{"r1"}, 1193 OptionalContexts: []string{"o1"}, 1194 FromBranchProtection: &no, 1195 }, 1196 Orgs: map[string]TideOrgContextPolicy{ 1197 "org": { 1198 TideContextPolicy: TideContextPolicy{ 1199 SkipUnknownContexts: &yes, 1200 RequiredContexts: []string{"r2"}, 1201 OptionalContexts: []string{"o2"}, 1202 FromBranchProtection: &yes, 1203 }, 1204 }, 1205 }, 1206 }, 1207 expected: TideContextPolicy{ 1208 SkipUnknownContexts: &yes, 1209 RequiredContexts: []string{"r1", "r2"}, 1210 OptionalContexts: []string{"o1", "o2"}, 1211 FromBranchProtection: &yes, 1212 }, 1213 }, 1214 { 1215 name: "repo config", 1216 config: TideContextPolicyOptions{ 1217 TideContextPolicy: TideContextPolicy{ 1218 RequiredContexts: []string{"r1"}, 1219 OptionalContexts: []string{"o1"}, 1220 FromBranchProtection: &no, 1221 }, 1222 Orgs: map[string]TideOrgContextPolicy{ 1223 "org": { 1224 TideContextPolicy: TideContextPolicy{ 1225 SkipUnknownContexts: &no, 1226 RequiredContexts: []string{"r2"}, 1227 OptionalContexts: []string{"o2"}, 1228 FromBranchProtection: &no, 1229 }, 1230 Repos: map[string]TideRepoContextPolicy{ 1231 "repo": { 1232 TideContextPolicy: TideContextPolicy{ 1233 SkipUnknownContexts: &yes, 1234 RequiredContexts: []string{"r3"}, 1235 OptionalContexts: []string{"o3"}, 1236 FromBranchProtection: &yes, 1237 }, 1238 }, 1239 }, 1240 }, 1241 }, 1242 }, 1243 expected: TideContextPolicy{ 1244 SkipUnknownContexts: &yes, 1245 RequiredContexts: []string{"r1", "r2", "r3"}, 1246 OptionalContexts: []string{"o1", "o2", "o3"}, 1247 FromBranchProtection: &yes, 1248 }, 1249 }, 1250 { 1251 name: "branch config", 1252 config: TideContextPolicyOptions{ 1253 TideContextPolicy: TideContextPolicy{ 1254 RequiredContexts: []string{"r1"}, 1255 OptionalContexts: []string{"o1"}, 1256 }, 1257 Orgs: map[string]TideOrgContextPolicy{ 1258 "org": { 1259 TideContextPolicy: TideContextPolicy{ 1260 RequiredContexts: []string{"r2"}, 1261 OptionalContexts: []string{"o2"}, 1262 }, 1263 Repos: map[string]TideRepoContextPolicy{ 1264 "repo": { 1265 TideContextPolicy: TideContextPolicy{ 1266 RequiredContexts: []string{"r3"}, 1267 OptionalContexts: []string{"o3"}, 1268 }, 1269 Branches: map[string]TideContextPolicy{ 1270 "branch": { 1271 RequiredContexts: []string{"r4"}, 1272 OptionalContexts: []string{"o4"}, 1273 }, 1274 }, 1275 }, 1276 }, 1277 }, 1278 }, 1279 }, 1280 expected: TideContextPolicy{ 1281 RequiredContexts: []string{"r1", "r2", "r3", "r4"}, 1282 OptionalContexts: []string{"o1", "o2", "o3", "o4"}, 1283 }, 1284 }, 1285 } 1286 for _, tc := range testCases { 1287 policy := ParseTideContextPolicyOptions(org, repo, branch, tc.config) 1288 if !reflect.DeepEqual(policy, tc.expected) { 1289 t.Errorf("%s - did not get expected policy: %s", tc.name, diff.ObjectReflectDiff(tc.expected, policy)) 1290 } 1291 } 1292 } 1293 1294 func TestConfigGetTideContextPolicy(t *testing.T) { 1295 yes := true 1296 no := false 1297 org, repo, branch := "org", "repo", "branch" 1298 testCases := []struct { 1299 name string 1300 config Config 1301 expected TideContextPolicy 1302 error string 1303 }{ 1304 { 1305 name: "no policy - use prow jobs", 1306 config: Config{ 1307 ProwConfig: ProwConfig{ 1308 BranchProtection: BranchProtection{ 1309 Policy: Policy{ 1310 Protect: &yes, 1311 RequiredStatusChecks: &ContextPolicy{ 1312 Contexts: []string{"r1", "r2"}, 1313 }, 1314 }, 1315 }, 1316 }, 1317 JobConfig: JobConfig{ 1318 PresubmitsStatic: map[string][]Presubmit{ 1319 "org/repo": { 1320 Presubmit{ 1321 Reporter: Reporter{ 1322 Context: "pr1", 1323 }, 1324 AlwaysRun: true, 1325 }, 1326 Presubmit{ 1327 Reporter: Reporter{ 1328 Context: "po1", 1329 }, 1330 AlwaysRun: true, 1331 Optional: true, 1332 }, 1333 }, 1334 }, 1335 }, 1336 }, 1337 expected: TideContextPolicy{ 1338 RequiredContexts: []string{"pr1"}, 1339 RequiredIfPresentContexts: []string{}, 1340 OptionalContexts: []string{"po1"}, 1341 }, 1342 }, 1343 { 1344 name: "no policy no prow jobs defined - empty", 1345 config: Config{ 1346 ProwConfig: ProwConfig{ 1347 BranchProtection: BranchProtection{ 1348 Policy: Policy{ 1349 Protect: &yes, 1350 RequiredStatusChecks: &ContextPolicy{ 1351 Contexts: []string{"r1", "r2"}, 1352 }, 1353 }, 1354 }, 1355 }, 1356 }, 1357 expected: TideContextPolicy{ 1358 RequiredContexts: []string{}, 1359 RequiredIfPresentContexts: []string{}, 1360 OptionalContexts: []string{}, 1361 }, 1362 }, 1363 { 1364 name: "no branch protection", 1365 config: Config{ 1366 ProwConfig: ProwConfig{ 1367 Tide: Tide{ 1368 TideGitHubConfig: TideGitHubConfig{ 1369 ContextOptions: TideContextPolicyOptions{ 1370 TideContextPolicy: TideContextPolicy{ 1371 FromBranchProtection: &yes, 1372 }, 1373 }, 1374 }, 1375 }, 1376 }, 1377 }, 1378 expected: TideContextPolicy{ 1379 RequiredContexts: []string{}, 1380 RequiredIfPresentContexts: []string{}, 1381 OptionalContexts: []string{}, 1382 }, 1383 }, 1384 { 1385 name: "invalid branch protection", 1386 config: Config{ 1387 ProwConfig: ProwConfig{ 1388 BranchProtection: BranchProtection{ 1389 Orgs: map[string]Org{ 1390 "org": { 1391 Policy: Policy{ 1392 Protect: &no, 1393 }, 1394 }, 1395 }, 1396 }, 1397 Tide: Tide{ 1398 TideGitHubConfig: TideGitHubConfig{ 1399 ContextOptions: TideContextPolicyOptions{ 1400 TideContextPolicy: TideContextPolicy{ 1401 FromBranchProtection: &yes, 1402 }, 1403 }, 1404 }, 1405 }, 1406 }, 1407 }, 1408 expected: TideContextPolicy{ 1409 RequiredContexts: []string{}, 1410 RequiredIfPresentContexts: []string{}, 1411 OptionalContexts: []string{}, 1412 }, 1413 }, 1414 { 1415 name: "branch protection with manually required triggered jobs", 1416 config: Config{ 1417 ProwConfig: ProwConfig{ 1418 BranchProtection: BranchProtection{ 1419 Orgs: map[string]Org{ 1420 "org": { 1421 Policy: Policy{ 1422 RequireManuallyTriggeredJobs: &yes, 1423 }, 1424 }, 1425 }, 1426 }, 1427 Tide: Tide{ 1428 TideGitHubConfig: TideGitHubConfig{ 1429 ContextOptions: TideContextPolicyOptions{ 1430 TideContextPolicy: TideContextPolicy{ 1431 FromBranchProtection: &yes, 1432 }, 1433 }, 1434 }, 1435 }, 1436 }, 1437 JobConfig: JobConfig{ 1438 PresubmitsStatic: map[string][]Presubmit{ 1439 "org/repo": { 1440 Presubmit{ 1441 Reporter: Reporter{ 1442 Context: "pr1", 1443 }, 1444 AlwaysRun: false, 1445 Optional: false, 1446 }, 1447 Presubmit{ 1448 Reporter: Reporter{ 1449 Context: "pr2", 1450 }, 1451 AlwaysRun: true, 1452 }, 1453 }, 1454 }, 1455 }, 1456 }, 1457 expected: TideContextPolicy{ 1458 RequiredContexts: []string{"pr1", "pr2"}, 1459 RequiredIfPresentContexts: []string{}, 1460 OptionalContexts: []string{}, 1461 }, 1462 }, 1463 { 1464 name: "manually defined policy", 1465 config: Config{ 1466 ProwConfig: ProwConfig{ 1467 Tide: Tide{ 1468 TideGitHubConfig: TideGitHubConfig{ 1469 ContextOptions: TideContextPolicyOptions{ 1470 TideContextPolicy: TideContextPolicy{ 1471 RequiredContexts: []string{"r1"}, 1472 RequiredIfPresentContexts: []string{}, 1473 OptionalContexts: []string{"o1"}, 1474 SkipUnknownContexts: &yes, 1475 }, 1476 }, 1477 }, 1478 }, 1479 }, 1480 }, 1481 expected: TideContextPolicy{ 1482 RequiredContexts: []string{"r1"}, 1483 RequiredIfPresentContexts: []string{}, 1484 OptionalContexts: []string{"o1"}, 1485 SkipUnknownContexts: &yes, 1486 }, 1487 }, 1488 { 1489 name: "jobs from inrepoconfig are considered", 1490 config: Config{ 1491 JobConfig: JobConfig{ 1492 ProwYAMLGetterWithDefaults: fakeProwYAMLGetterFactory( 1493 []Presubmit{ 1494 { 1495 AlwaysRun: true, 1496 Reporter: Reporter{Context: "ir0"}, 1497 }, 1498 { 1499 AlwaysRun: true, 1500 Optional: true, 1501 Reporter: Reporter{Context: "ir1"}, 1502 }, 1503 }, 1504 nil, 1505 ), 1506 }, 1507 ProwConfig: ProwConfig{ 1508 InRepoConfig: InRepoConfig{ 1509 Enabled: map[string]*bool{"*": utilpointer.Bool(true)}, 1510 }, 1511 }, 1512 }, 1513 expected: TideContextPolicy{ 1514 RequiredContexts: []string{"ir0"}, 1515 RequiredIfPresentContexts: []string{}, 1516 OptionalContexts: []string{"ir1"}, 1517 }, 1518 }, 1519 { 1520 name: "both static and inrepoconfig jobs are consired", 1521 config: Config{ 1522 JobConfig: JobConfig{ 1523 PresubmitsStatic: map[string][]Presubmit{ 1524 "org/repo": { 1525 Presubmit{ 1526 Reporter: Reporter{ 1527 Context: "pr1", 1528 }, 1529 AlwaysRun: true, 1530 }, 1531 Presubmit{ 1532 Reporter: Reporter{ 1533 Context: "po1", 1534 }, 1535 AlwaysRun: true, 1536 Optional: true, 1537 }, 1538 }, 1539 }, 1540 ProwYAMLGetterWithDefaults: fakeProwYAMLGetterFactory( 1541 []Presubmit{ 1542 { 1543 AlwaysRun: true, 1544 Reporter: Reporter{Context: "ir0"}, 1545 }, 1546 { 1547 AlwaysRun: true, 1548 Optional: true, 1549 Reporter: Reporter{Context: "ir1"}, 1550 }, 1551 }, 1552 nil, 1553 ), 1554 }, 1555 ProwConfig: ProwConfig{ 1556 InRepoConfig: InRepoConfig{ 1557 Enabled: map[string]*bool{"*": utilpointer.Bool(true)}, 1558 }, 1559 }, 1560 }, 1561 expected: TideContextPolicy{ 1562 RequiredContexts: []string{"ir0", "pr1"}, 1563 RequiredIfPresentContexts: []string{}, 1564 OptionalContexts: []string{"ir1", "po1"}, 1565 }, 1566 }, 1567 } 1568 1569 for _, tc := range testCases { 1570 t.Run(tc.name, func(t *testing.T) { 1571 1572 baseSHAGetter := func() (string, error) { 1573 return "baseSHA", nil 1574 } 1575 p, err := tc.config.GetTideContextPolicy(nil, org, repo, branch, baseSHAGetter, "some-sha") 1576 if !reflect.DeepEqual(p, &tc.expected) { 1577 t.Errorf("%s - did not get expected policy: %s", tc.name, diff.ObjectReflectDiff(&tc.expected, p)) 1578 } 1579 if err != nil { 1580 if err.Error() != tc.error { 1581 t.Errorf("%s - expected error %v got %v", tc.name, tc.error, err.Error()) 1582 } 1583 } else if tc.error != "" { 1584 t.Errorf("%s - expected error %v got nil", tc.name, tc.error) 1585 } 1586 }) 1587 } 1588 } 1589 1590 func TestMergeTideContextPolicyConfig(t *testing.T) { 1591 yes := true 1592 no := false 1593 testCases := []struct { 1594 name string 1595 a, b, c TideContextPolicy 1596 }{ 1597 { 1598 name: "all empty", 1599 }, 1600 { 1601 name: "empty a", 1602 b: TideContextPolicy{ 1603 SkipUnknownContexts: &yes, 1604 FromBranchProtection: &no, 1605 RequiredContexts: []string{"r1"}, 1606 OptionalContexts: []string{"o1"}, 1607 }, 1608 c: TideContextPolicy{ 1609 SkipUnknownContexts: &yes, 1610 FromBranchProtection: &no, 1611 RequiredContexts: []string{"r1"}, 1612 OptionalContexts: []string{"o1"}, 1613 }, 1614 }, 1615 { 1616 name: "empty b", 1617 a: TideContextPolicy{ 1618 SkipUnknownContexts: &yes, 1619 FromBranchProtection: &no, 1620 RequiredContexts: []string{"r1"}, 1621 OptionalContexts: []string{"o1"}, 1622 }, 1623 c: TideContextPolicy{ 1624 SkipUnknownContexts: &yes, 1625 FromBranchProtection: &no, 1626 RequiredContexts: []string{"r1"}, 1627 OptionalContexts: []string{"o1"}, 1628 }, 1629 }, 1630 { 1631 name: "merging unset boolean", 1632 a: TideContextPolicy{ 1633 FromBranchProtection: &no, 1634 RequiredContexts: []string{"r1"}, 1635 OptionalContexts: []string{"o1"}, 1636 }, 1637 b: TideContextPolicy{ 1638 SkipUnknownContexts: &yes, 1639 RequiredContexts: []string{"r2"}, 1640 OptionalContexts: []string{"o2"}, 1641 }, 1642 c: TideContextPolicy{ 1643 SkipUnknownContexts: &yes, 1644 FromBranchProtection: &no, 1645 RequiredContexts: []string{"r1", "r2"}, 1646 OptionalContexts: []string{"o1", "o2"}, 1647 }, 1648 }, 1649 { 1650 name: "merging unset contexts in a", 1651 a: TideContextPolicy{ 1652 FromBranchProtection: &no, 1653 SkipUnknownContexts: &yes, 1654 }, 1655 b: TideContextPolicy{ 1656 FromBranchProtection: &yes, 1657 SkipUnknownContexts: &no, 1658 RequiredContexts: []string{"r1"}, 1659 OptionalContexts: []string{"o1"}, 1660 }, 1661 c: TideContextPolicy{ 1662 FromBranchProtection: &yes, 1663 SkipUnknownContexts: &no, 1664 RequiredContexts: []string{"r1"}, 1665 OptionalContexts: []string{"o1"}, 1666 }, 1667 }, 1668 { 1669 name: "merging unset contexts in b", 1670 a: TideContextPolicy{ 1671 FromBranchProtection: &yes, 1672 SkipUnknownContexts: &no, 1673 RequiredContexts: []string{"r1"}, 1674 OptionalContexts: []string{"o1"}, 1675 }, 1676 b: TideContextPolicy{ 1677 FromBranchProtection: &no, 1678 SkipUnknownContexts: &yes, 1679 }, 1680 c: TideContextPolicy{ 1681 FromBranchProtection: &no, 1682 SkipUnknownContexts: &yes, 1683 RequiredContexts: []string{"r1"}, 1684 OptionalContexts: []string{"o1"}, 1685 }, 1686 }, 1687 } 1688 1689 for _, tc := range testCases { 1690 c := mergeTideContextPolicy(tc.a, tc.b) 1691 if !reflect.DeepEqual(c, tc.c) { 1692 t.Errorf("%s - expected %v got %v", tc.name, tc.c, c) 1693 } 1694 } 1695 } 1696 1697 func TestTideQuery_Validate(t *testing.T) { 1698 testCases := []struct { 1699 name string 1700 query TideQuery 1701 expectError bool 1702 }{ 1703 { 1704 name: "good query", 1705 query: TideQuery{ 1706 Orgs: []string{"kuber"}, 1707 Repos: []string{"foo/bar", "baz/bar"}, 1708 ExcludedRepos: []string{"kuber/netes"}, 1709 IncludedBranches: []string{"master"}, 1710 Milestone: "backlog-forever", 1711 Labels: []string{labels.LGTM, labels.Approved}, 1712 MissingLabels: []string{"do-not-merge/evil-code"}, 1713 ReviewApprovedRequired: true, 1714 }, 1715 expectError: false, 1716 }, 1717 { 1718 name: "simple org query is valid", 1719 query: TideQuery{ 1720 Orgs: []string{"kuber"}, 1721 }, 1722 expectError: false, 1723 }, 1724 { 1725 name: "org with slash is invalid", 1726 query: TideQuery{ 1727 Orgs: []string{"kube/r"}, 1728 }, 1729 expectError: true, 1730 }, 1731 { 1732 name: "empty org is invalid", 1733 query: TideQuery{ 1734 Orgs: []string{""}, 1735 }, 1736 expectError: true, 1737 }, 1738 { 1739 name: "duplicate org is invalid", 1740 query: TideQuery{ 1741 Orgs: []string{"kuber", "kuber"}, 1742 }, 1743 expectError: true, 1744 }, 1745 { 1746 name: "simple repo query is valid", 1747 query: TideQuery{ 1748 Repos: []string{"kuber/netes"}, 1749 }, 1750 expectError: false, 1751 }, 1752 { 1753 name: "repo without slash is invalid", 1754 query: TideQuery{ 1755 Repos: []string{"foobar", "baz/bar"}, 1756 }, 1757 expectError: true, 1758 }, 1759 { 1760 name: "repo included with parent org is invalid", 1761 query: TideQuery{ 1762 Orgs: []string{"kuber"}, 1763 Repos: []string{"foo/bar", "kuber/netes"}, 1764 }, 1765 expectError: true, 1766 }, 1767 { 1768 name: "duplicate repo is invalid", 1769 query: TideQuery{ 1770 Repos: []string{"baz/bar", "foo/bar", "baz/bar"}, 1771 }, 1772 expectError: true, 1773 }, 1774 { 1775 name: "empty orgs and repos is invalid", 1776 query: TideQuery{ 1777 IncludedBranches: []string{"master"}, 1778 Milestone: "backlog-forever", 1779 Labels: []string{labels.LGTM, labels.Approved}, 1780 MissingLabels: []string{"do-not-merge/evil-code"}, 1781 ReviewApprovedRequired: true, 1782 }, 1783 expectError: true, 1784 }, 1785 { 1786 name: "simple excluded repo query is valid", 1787 query: TideQuery{ 1788 Orgs: []string{"kuber"}, 1789 ExcludedRepos: []string{"kuber/netes"}, 1790 }, 1791 expectError: false, 1792 }, 1793 { 1794 name: "excluded repo without slash is invalid", 1795 query: TideQuery{ 1796 Orgs: []string{"kuber"}, 1797 ExcludedRepos: []string{"kubernetes"}, 1798 }, 1799 expectError: true, 1800 }, 1801 { 1802 name: "excluded repo included without parent org is invalid", 1803 query: TideQuery{ 1804 Repos: []string{"foo/bar", "baz/bar"}, 1805 ExcludedRepos: []string{"kuber/netes"}, 1806 }, 1807 expectError: true, 1808 }, 1809 { 1810 name: "duplicate excluded repo is invalid", 1811 query: TideQuery{ 1812 Orgs: []string{"kuber"}, 1813 ExcludedRepos: []string{"kuber/netes", "kuber/netes"}, 1814 ReviewApprovedRequired: true, 1815 }, 1816 expectError: true, 1817 }, 1818 { 1819 name: "label cannot be required and forbidden", 1820 query: TideQuery{ 1821 Orgs: []string{"kuber"}, 1822 Labels: []string{labels.LGTM, labels.Approved}, 1823 MissingLabels: []string{"do-not-merge/evil-code", labels.LGTM}, 1824 }, 1825 expectError: true, 1826 }, 1827 { 1828 name: "simple excluded branches query is valid", 1829 query: TideQuery{ 1830 Orgs: []string{"kuber"}, 1831 ExcludedBranches: []string{"dev"}, 1832 }, 1833 expectError: false, 1834 }, 1835 { 1836 name: "specifying both included and excluded branches is invalid", 1837 query: TideQuery{ 1838 Orgs: []string{"kuber"}, 1839 IncludedBranches: []string{"master"}, 1840 ExcludedBranches: []string{"dev"}, 1841 }, 1842 expectError: true, 1843 }, 1844 } 1845 for _, tc := range testCases { 1846 t.Run(tc.name, func(t *testing.T) { 1847 err := tc.query.Validate() 1848 if err != nil && !tc.expectError { 1849 t.Errorf("Unexpected error: %v.", err) 1850 } else if err == nil && tc.expectError { 1851 t.Error("Expected a validation error, but didn't get one.") 1852 } 1853 }) 1854 1855 } 1856 } 1857 1858 func TestTideContextPolicy_Validate(t *testing.T) { 1859 testCases := []struct { 1860 name string 1861 t TideContextPolicy 1862 failed bool 1863 }{ 1864 { 1865 name: "good policy", 1866 t: TideContextPolicy{ 1867 OptionalContexts: []string{"o1"}, 1868 RequiredContexts: []string{"r1"}, 1869 }, 1870 }, 1871 { 1872 name: "optional contexts must differ from required contexts", 1873 t: TideContextPolicy{ 1874 OptionalContexts: []string{"c1"}, 1875 RequiredContexts: []string{"c1"}, 1876 }, 1877 failed: true, 1878 }, 1879 { 1880 name: "individual contexts cannot be both optional and required", 1881 t: TideContextPolicy{ 1882 OptionalContexts: []string{"c1", "c2", "c3", "c4"}, 1883 RequiredContexts: []string{"c1", "c4"}, 1884 }, 1885 failed: true, 1886 }, 1887 } 1888 for _, tc := range testCases { 1889 err := tc.t.Validate() 1890 failed := err != nil 1891 if failed != tc.failed { 1892 t.Errorf("%s - expected %v got %v", tc.name, tc.failed, err) 1893 } 1894 } 1895 } 1896 1897 func TestTideContextPolicy_IsOptional(t *testing.T) { 1898 testCases := []struct { 1899 name string 1900 skipUnknownContexts bool 1901 required, optional []string 1902 contexts []string 1903 results []bool 1904 }{ 1905 { 1906 name: "only optional contexts registered - skipUnknownContexts false", 1907 contexts: []string{"c1", "o1", "o2"}, 1908 optional: []string{"o1", "o2"}, 1909 results: []bool{false, true, true}, 1910 }, 1911 { 1912 name: "no contexts registered - skipUnknownContexts false", 1913 contexts: []string{"t2"}, 1914 results: []bool{false}, 1915 }, 1916 { 1917 name: "only required contexts registered - skipUnknownContexts false", 1918 required: []string{"c1", "c2", "c3"}, 1919 contexts: []string{"c1", "c2", "c3", "t1"}, 1920 results: []bool{false, false, false, false}, 1921 }, 1922 { 1923 name: "optional and required contexts registered - skipUnknownContexts false", 1924 optional: []string{"o1", "o2"}, 1925 required: []string{"c1", "c2", "c3"}, 1926 contexts: []string{"o1", "o2", "c1", "c2", "c3", "t1"}, 1927 results: []bool{true, true, false, false, false, false}, 1928 }, 1929 { 1930 name: "only optional contexts registered - skipUnknownContexts true", 1931 contexts: []string{"c1", "o1", "o2"}, 1932 optional: []string{"o1", "o2"}, 1933 skipUnknownContexts: true, 1934 results: []bool{true, true, true}, 1935 }, 1936 { 1937 name: "no contexts registered - skipUnknownContexts true", 1938 contexts: []string{"t2"}, 1939 skipUnknownContexts: true, 1940 results: []bool{true}, 1941 }, 1942 { 1943 name: "only required contexts registered - skipUnknownContexts true", 1944 required: []string{"c1", "c2", "c3"}, 1945 contexts: []string{"c1", "c2", "c3", "t1"}, 1946 skipUnknownContexts: true, 1947 results: []bool{false, false, false, true}, 1948 }, 1949 { 1950 name: "optional and required contexts registered - skipUnknownContexts true", 1951 optional: []string{"o1", "o2"}, 1952 required: []string{"c1", "c2", "c3"}, 1953 contexts: []string{"o1", "o2", "c1", "c2", "c3", "t1"}, 1954 skipUnknownContexts: true, 1955 results: []bool{true, true, false, false, false, true}, 1956 }, 1957 } 1958 1959 for _, tc := range testCases { 1960 cp := TideContextPolicy{ 1961 SkipUnknownContexts: &tc.skipUnknownContexts, 1962 RequiredContexts: tc.required, 1963 OptionalContexts: tc.optional, 1964 } 1965 for i, c := range tc.contexts { 1966 if cp.IsOptional(c) != tc.results[i] { 1967 t.Errorf("%s - IsOptional for %s should return %t", tc.name, c, tc.results[i]) 1968 } 1969 } 1970 } 1971 } 1972 1973 func TestTideContextPolicy_MissingRequiredContexts(t *testing.T) { 1974 testCases := []struct { 1975 name string 1976 skipUnknownContexts bool 1977 required, optional []string 1978 existingContexts, expectedContexts []string 1979 }{ 1980 { 1981 name: "no contexts registered", 1982 existingContexts: []string{"c1", "c2"}, 1983 }, 1984 { 1985 name: "optional contexts registered / no missing contexts", 1986 optional: []string{"o1", "o2", "o3"}, 1987 existingContexts: []string{"c1", "c2"}, 1988 }, 1989 { 1990 name: "required contexts registered / missing contexts", 1991 required: []string{"c1", "c2", "c3"}, 1992 existingContexts: []string{"c1", "c2"}, 1993 expectedContexts: []string{"c3"}, 1994 }, 1995 { 1996 name: "required contexts registered / no missing contexts", 1997 required: []string{"c1", "c2", "c3"}, 1998 existingContexts: []string{"c1", "c2", "c3"}, 1999 }, 2000 { 2001 name: "optional and required contexts registered / missing contexts", 2002 optional: []string{"o1", "o2", "o3"}, 2003 required: []string{"c1", "c2", "c3"}, 2004 existingContexts: []string{"c1", "c2"}, 2005 expectedContexts: []string{"c3"}, 2006 }, 2007 { 2008 name: "optional and required contexts registered / no missing contexts", 2009 optional: []string{"o1", "o2", "o3"}, 2010 required: []string{"c1", "c2"}, 2011 existingContexts: []string{"c1", "c2", "c4"}, 2012 }, 2013 } 2014 2015 for _, tc := range testCases { 2016 cp := TideContextPolicy{ 2017 SkipUnknownContexts: &tc.skipUnknownContexts, 2018 RequiredContexts: tc.required, 2019 OptionalContexts: tc.optional, 2020 } 2021 missingContexts := cp.MissingRequiredContexts(tc.existingContexts) 2022 if !sets.New[string](missingContexts...).Equal(sets.New[string](tc.expectedContexts...)) { 2023 t.Errorf("%s - expected %v got %v", tc.name, tc.expectedContexts, missingContexts) 2024 } 2025 } 2026 } 2027 2028 func fakeProwYAMLGetterFactory(presubmits []Presubmit, postsubmits []Postsubmit) ProwYAMLGetter { 2029 return func(_ *Config, _ git.ClientFactory, _, _, _ string, _ ...string) (*ProwYAML, error) { 2030 return &ProwYAML{ 2031 Presubmits: presubmits, 2032 Postsubmits: postsubmits, 2033 }, nil 2034 } 2035 }