sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/statusreconciler/controller_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 statusreconciler 18 19 import ( 20 "errors" 21 "testing" 22 23 "github.com/google/go-cmp/cmp" 24 "github.com/google/go-cmp/cmp/cmpopts" 25 "github.com/sirupsen/logrus" 26 "k8s.io/apimachinery/pkg/util/sets" 27 "sigs.k8s.io/yaml" 28 29 "sigs.k8s.io/prow/pkg/config" 30 "sigs.k8s.io/prow/pkg/github" 31 ) 32 33 var ignoreUnexported = cmpopts.IgnoreUnexported(config.Presubmit{}, config.RegexpChangeMatcher{}, config.Brancher{}) 34 35 func TestAddedBlockingPresubmits(t *testing.T) { 36 var testCases = []struct { 37 name string 38 old, new string 39 expected map[string][]config.Presubmit 40 }{ 41 { 42 name: "no change in blocking presubmits means no added blocking jobs", 43 old: `"org/repo": 44 - name: old-job 45 context: old-context 46 always_run: true`, 47 new: `"org/repo": 48 - name: old-job 49 context: old-context 50 always_run: true`, 51 expected: map[string][]config.Presubmit{ 52 "org/repo": {}, 53 }, 54 }, 55 { 56 name: "added optional presubmit means no added blocking jobs", 57 old: `"org/repo": 58 - name: old-job 59 context: old-context 60 always_run: true`, 61 new: `"org/repo": 62 - name: old-job 63 context: old-context 64 always_run: true 65 - name: new-job 66 context: new-context 67 always_run: true 68 optional: true`, 69 expected: map[string][]config.Presubmit{ 70 "org/repo": {}, 71 }, 72 }, 73 { 74 name: "added non-reporting presubmit means no added blocking jobs", 75 old: `"org/repo": 76 - name: old-job 77 context: old-context 78 always_run: true`, 79 new: `"org/repo": 80 - name: old-job 81 context: old-context 82 always_run: true 83 - name: new-job 84 context: new-context 85 always_run: true 86 skip_report: true`, 87 expected: map[string][]config.Presubmit{ 88 "org/repo": {}, 89 }, 90 }, 91 { 92 name: "added presubmit that needs a manual trigger means no added blocking jobs", 93 old: `"org/repo": 94 - name: old-job 95 context: old-context 96 always_run: true`, 97 new: `"org/repo": 98 - name: old-job 99 context: old-context 100 always_run: true 101 - name: new-job 102 context: new-context 103 always_run: false`, 104 expected: map[string][]config.Presubmit{ 105 "org/repo": {}, 106 }, 107 }, 108 { 109 name: "added required presubmit means added blocking jobs", 110 old: `"org/repo": 111 - name: old-job 112 context: old-context 113 always_run: true`, 114 new: `"org/repo": 115 - name: old-job 116 context: old-context 117 always_run: true 118 - name: new-job 119 context: new-context 120 always_run: true`, 121 expected: map[string][]config.Presubmit{ 122 "org/repo": {{ 123 JobBase: config.JobBase{Name: "new-job"}, 124 Reporter: config.Reporter{ 125 Context: "new-context", 126 SkipReport: false, 127 }, 128 AlwaysRun: true, 129 Optional: false, 130 }}, 131 }, 132 }, 133 { 134 name: "optional presubmit transitioning to required means no added blocking jobs", 135 old: `"org/repo": 136 - name: old-job 137 context: old-context 138 always_run: true 139 optional: true`, 140 new: `"org/repo": 141 - name: old-job 142 context: old-context 143 always_run: true`, 144 expected: map[string][]config.Presubmit{ 145 "org/repo": {}, 146 }, 147 }, 148 { 149 name: "non-reporting presubmit transitioning to required means added blocking jobs", 150 old: `"org/repo": 151 - name: old-job 152 context: old-context 153 always_run: true 154 skip_report: true`, 155 new: `"org/repo": 156 - name: old-job 157 context: old-context 158 always_run: true`, 159 expected: map[string][]config.Presubmit{ 160 "org/repo": {{ 161 JobBase: config.JobBase{Name: "old-job"}, 162 Reporter: config.Reporter{Context: "old-context"}, 163 AlwaysRun: true, 164 }}, 165 }, 166 }, 167 { 168 name: "required presubmit transitioning run_if_changed means added blocking jobs", 169 old: `"org/repo": 170 - name: old-job 171 context: old-context 172 run_if_changed: old-changes`, 173 new: `"org/repo": 174 - name: old-job 175 context: old-context 176 run_if_changed: new-changes`, 177 expected: map[string][]config.Presubmit{ 178 "org/repo": {{ 179 JobBase: config.JobBase{Name: "old-job"}, 180 Reporter: config.Reporter{Context: "old-context"}, 181 RegexpChangeMatcher: config.RegexpChangeMatcher{RunIfChanged: "new-changes"}, 182 }}, 183 }, 184 }, 185 { 186 name: "optional presubmit transitioning run_if_changed means no added blocking jobs", 187 old: `"org/repo": 188 - name: old-job 189 context: old-context 190 run_if_changed: old-changes 191 optional: true`, 192 new: `"org/repo": 193 - name: old-job 194 context: old-context 195 run_if_changed: new-changes 196 optional: true`, 197 expected: map[string][]config.Presubmit{ 198 "org/repo": {}, 199 }, 200 }, 201 { 202 name: "optional presubmit transitioning to required run_if_changed means added blocking jobs", 203 old: `"org/repo": 204 - name: old-job 205 context: old-context 206 always_run: true 207 optional: true`, 208 new: `"org/repo": 209 - name: old-job 210 context: old-context 211 run_if_changed: changes`, 212 expected: map[string][]config.Presubmit{ 213 "org/repo": {{ 214 JobBase: config.JobBase{Name: "old-job"}, 215 Reporter: config.Reporter{Context: "old-context"}, 216 RegexpChangeMatcher: config.RegexpChangeMatcher{RunIfChanged: "changes"}, 217 }}, 218 }, 219 }, 220 { 221 name: "required presubmit transitioning to new context means no added blocking jobs", 222 old: `"org/repo": 223 - name: old-job 224 context: old-context 225 always_run: true`, 226 new: `"org/repo": 227 - name: old-job 228 context: new-context 229 always_run: true`, 230 expected: map[string][]config.Presubmit{ 231 "org/repo": {}, 232 }, 233 }, 234 } 235 236 for _, testCase := range testCases { 237 t.Run(testCase.name, func(t *testing.T) { 238 var oldConfig, newConfig map[string][]config.Presubmit 239 if err := yaml.Unmarshal([]byte(testCase.old), &oldConfig); err != nil { 240 t.Fatalf("%s: could not unmarshal old config: %v", testCase.name, err) 241 } 242 if err := yaml.Unmarshal([]byte(testCase.new), &newConfig); err != nil { 243 t.Fatalf("%s: could not unmarshal new config: %v", testCase.name, err) 244 } 245 actual, _ := addedBlockingPresubmits(oldConfig, newConfig, logrusEntry()) 246 if diff := cmp.Diff(actual, testCase.expected, ignoreUnexported); diff != "" { 247 t.Errorf("%s: did not get correct added presubmits: %v", testCase.name, diff) 248 } 249 }) 250 } 251 } 252 253 func TestRemovedPresubmits(t *testing.T) { 254 var testCases = []struct { 255 name string 256 old, new string 257 expected map[string][]config.Presubmit 258 }{ 259 { 260 name: "no change in blocking presubmits means no removed jobs", 261 old: `"org/repo": 262 - name: old-job 263 context: old-context`, 264 new: `"org/repo": 265 - name: old-job 266 context: old-context`, 267 expected: map[string][]config.Presubmit{ 268 "org/repo": {}, 269 }, 270 }, 271 { 272 name: "removed optional presubmit means removed job", 273 old: `"org/repo": 274 - name: old-job 275 context: old-context 276 optional: true`, 277 new: `"org/repo": []`, 278 expected: map[string][]config.Presubmit{ 279 "org/repo": {{ 280 JobBase: config.JobBase{Name: "old-job"}, 281 Reporter: config.Reporter{Context: "old-context"}, 282 Optional: true, 283 }}, 284 }, 285 }, 286 { 287 name: "removed non-reporting presubmit means removed job", 288 old: `"org/repo": 289 - name: old-job 290 context: old-context 291 skip_report: true`, 292 new: `"org/repo": []`, 293 expected: map[string][]config.Presubmit{ 294 "org/repo": {{ 295 JobBase: config.JobBase{Name: "old-job"}, 296 Reporter: config.Reporter{Context: "old-context", SkipReport: true}, 297 }}, 298 }, 299 }, 300 { 301 name: "removed required presubmit means removed jobs", 302 old: `"org/repo": 303 - name: old-job 304 context: old-context`, 305 new: `"org/repo": []`, 306 expected: map[string][]config.Presubmit{ 307 "org/repo": {{ 308 JobBase: config.JobBase{Name: "old-job"}, 309 Reporter: config.Reporter{Context: "old-context"}, 310 }}, 311 }, 312 }, 313 { 314 name: "required presubmit transitioning to optional means no removed jobs", 315 old: `"org/repo": 316 - name: old-job 317 context: old-context`, 318 new: `"org/repo": 319 - name: old-job 320 context: old-context 321 optional: true`, 322 expected: map[string][]config.Presubmit{ 323 "org/repo": {}, 324 }, 325 }, 326 { 327 name: "reporting presubmit transitioning to non-reporting means no removed jobs", 328 old: `"org/repo": 329 - name: old-job 330 context: old-context`, 331 new: `"org/repo": 332 - name: old-job 333 context: old-context 334 skip_report: true`, 335 expected: map[string][]config.Presubmit{ 336 "org/repo": {}, 337 }, 338 }, 339 { 340 name: "all presubmits removed means removed jobs", 341 old: `"org/repo": 342 - name: old-job 343 context: old-context`, 344 new: `{}`, 345 expected: map[string][]config.Presubmit{ 346 "org/repo": {{ 347 JobBase: config.JobBase{Name: "old-job"}, 348 Reporter: config.Reporter{Context: "old-context"}, 349 }}, 350 }, 351 }, 352 { 353 name: "required presubmit transitioning to new context means no removed jobs", 354 old: `"org/repo": 355 - name: old-job 356 context: old-context`, 357 new: `"org/repo": 358 - name: old-job 359 context: new-context`, 360 expected: map[string][]config.Presubmit{ 361 "org/repo": {}, 362 }, 363 }, 364 { 365 name: "required presubmit transitioning run_if_changed means no removed jobs", 366 old: `"org/repo": 367 - name: old-job 368 context: old-context 369 run_if_changed: old-changes`, 370 new: `"org/repo": 371 - name: old-job 372 context: old-context 373 run_if_changed: new-changes`, 374 expected: map[string][]config.Presubmit{ 375 "org/repo": {}, 376 }, 377 }, 378 { 379 name: "optional presubmit transitioning to required run_if_changed means no removed jobs", 380 old: `"org/repo": 381 - name: old-job 382 context: old-context 383 optional: true`, 384 new: `"org/repo": 385 - name: old-job 386 context: old-context 387 run_if_changed: changes`, 388 expected: map[string][]config.Presubmit{ 389 "org/repo": {}, 390 }, 391 }, 392 } 393 394 for _, testCase := range testCases { 395 t.Run(testCase.name, func(t *testing.T) { 396 var oldConfig, newConfig map[string][]config.Presubmit 397 if err := yaml.Unmarshal([]byte(testCase.old), &oldConfig); err != nil { 398 t.Fatalf("%s: could not unmarshal old config: %v", testCase.name, err) 399 } 400 if err := yaml.Unmarshal([]byte(testCase.new), &newConfig); err != nil { 401 t.Fatalf("%s: could not unmarshal new config: %v", testCase.name, err) 402 } 403 actual, _ := removedPresubmits(oldConfig, newConfig, logrusEntry()) 404 if diff := cmp.Diff(actual, testCase.expected, ignoreUnexported); diff != "" { 405 t.Errorf("%s: did not get correct removed presubmits: %v", testCase.name, diff) 406 } 407 }) 408 } 409 } 410 411 func TestMigratedBlockingPresubmits(t *testing.T) { 412 var testCases = []struct { 413 name string 414 old, new string 415 expected map[string][]presubmitMigration 416 }{ 417 { 418 name: "no change in blocking presubmits means no migrated blocking jobs", 419 old: `"org/repo": 420 - name: old-job 421 context: old-context`, 422 new: `"org/repo": 423 - name: old-job 424 context: old-context`, 425 expected: map[string][]presubmitMigration{ 426 "org/repo": {}, 427 }, 428 }, 429 { 430 name: "removed optional presubmit means no migrated blocking jobs", 431 old: `"org/repo": 432 - name: old-job 433 context: old-context 434 optional: true`, 435 new: `"org/repo": []`, 436 expected: map[string][]presubmitMigration{ 437 "org/repo": {}, 438 }, 439 }, 440 { 441 name: "removed non-reporting presubmit means no migrated blocking jobs", 442 old: `"org/repo": 443 - name: old-job 444 context: old-context 445 skip_report: true`, 446 new: `"org/repo": []`, 447 expected: map[string][]presubmitMigration{ 448 "org/repo": {}, 449 }, 450 }, 451 { 452 name: "removed required presubmit means no migrated blocking jobs", 453 old: `"org/repo": 454 - name: old-job 455 context: old-context`, 456 new: `"org/repo": []`, 457 expected: map[string][]presubmitMigration{ 458 "org/repo": {}, 459 }, 460 }, 461 { 462 name: "required presubmit transitioning to optional means no migrated blocking jobs", 463 old: `"org/repo": 464 - name: old-job 465 context: old-context`, 466 new: `"org/repo": 467 - name: old-job 468 context: old-context 469 optional: true`, 470 expected: map[string][]presubmitMigration{ 471 "org/repo": {}, 472 }, 473 }, 474 { 475 name: "reporting presubmit transitioning to non-reporting means no migrated blocking jobs", 476 old: `"org/repo": 477 - name: old-job 478 context: old-context`, 479 new: `"org/repo": 480 - name: old-job 481 context: old-context 482 skip_report: true`, 483 expected: map[string][]presubmitMigration{ 484 "org/repo": {}, 485 }, 486 }, 487 { 488 name: "all presubmits removed means no migrated blocking jobs", 489 old: `"org/repo": 490 - name: old-job 491 context: old-context`, 492 new: `{}`, 493 expected: map[string][]presubmitMigration{ 494 "org/repo": {}, 495 }, 496 }, 497 { 498 name: "required presubmit transitioning to new context means migrated blocking jobs", 499 old: `"org/repo": 500 - name: old-job 501 context: old-context`, 502 new: `"org/repo": 503 - name: old-job 504 context: new-context`, 505 expected: map[string][]presubmitMigration{ 506 "org/repo": {{ 507 from: config.Presubmit{ 508 JobBase: config.JobBase{Name: "old-job"}, 509 Reporter: config.Reporter{Context: "old-context"}, 510 }, 511 to: config.Presubmit{ 512 JobBase: config.JobBase{Name: "old-job"}, 513 Reporter: config.Reporter{Context: "new-context"}, 514 }, 515 }}, 516 }, 517 }, 518 { 519 name: "required presubmit transitioning run_if_changed means no removed blocking jobs", 520 old: `"org/repo": 521 - name: old-job 522 context: old-context 523 run_if_changed: old-changes`, 524 new: `"org/repo": 525 - name: old-job 526 context: old-context 527 run_if_changed: new-changes`, 528 expected: map[string][]presubmitMigration{ 529 "org/repo": {}, 530 }, 531 }, 532 { 533 name: "optional presubmit transitioning to required run_if_changed means no removed blocking jobs", 534 old: `"org/repo": 535 - name: old-job 536 context: old-context 537 optional: true`, 538 new: `"org/repo": 539 - name: old-job 540 context: old-context 541 run_if_changed: changes`, 542 expected: map[string][]presubmitMigration{ 543 "org/repo": {}, 544 }, 545 }, 546 } 547 548 for _, testCase := range testCases { 549 t.Run(testCase.name, func(t *testing.T) { 550 var oldConfig, newConfig map[string][]config.Presubmit 551 if err := yaml.Unmarshal([]byte(testCase.old), &oldConfig); err != nil { 552 t.Fatalf("%s: could not unmarshal old config: %v", testCase.name, err) 553 } 554 if err := yaml.Unmarshal([]byte(testCase.new), &newConfig); err != nil { 555 t.Fatalf("%s: could not unmarshal new config: %v", testCase.name, err) 556 } 557 actual, _ := migratedBlockingPresubmits(oldConfig, newConfig, logrusEntry()) 558 if diff := cmp.Diff(actual, testCase.expected, ignoreUnexported, cmp.AllowUnexported(presubmitMigration{})); diff != "" { 559 t.Errorf("%s: did not get correct removed presubmits: %v", testCase.name, diff) 560 } 561 }) 562 } 563 } 564 565 type orgRepo struct { 566 org, repo string 567 } 568 569 type orgRepoSet map[orgRepo]interface{} 570 571 func (s orgRepoSet) has(item orgRepo) bool { 572 _, contained := s[item] 573 return contained 574 } 575 576 type migration struct { 577 from, to string 578 } 579 580 type migrationSet map[migration]interface{} 581 582 func (s migrationSet) insert(items ...migration) { 583 for _, item := range items { 584 s[item] = nil 585 } 586 } 587 588 func (s migrationSet) has(item migration) bool { 589 _, contained := s[item] 590 return contained 591 } 592 593 func newFakeMigrator(key orgRepo) fakeMigrator { 594 return fakeMigrator{ 595 retireErrors: map[orgRepo]sets.Set[string]{key: sets.New[string]()}, 596 migrateErrors: map[orgRepo]migrationSet{key: {}}, 597 retired: map[orgRepo]sets.Set[string]{key: sets.New[string]()}, 598 migrated: map[orgRepo]migrationSet{key: {}}, 599 } 600 } 601 602 type fakeMigrator struct { 603 retireErrors map[orgRepo]sets.Set[string] 604 migrateErrors map[orgRepo]migrationSet 605 606 retired map[orgRepo]sets.Set[string] 607 migrated map[orgRepo]migrationSet 608 } 609 610 func (m *fakeMigrator) retire(org, repo, context string, _ func(string) bool) error { 611 key := orgRepo{org: org, repo: repo} 612 if contexts, exist := m.retireErrors[key]; exist && contexts.Has(context) { 613 return errors.New("failed to retire context") 614 } 615 if _, exist := m.retired[key]; exist { 616 m.retired[key].Insert(context) 617 } else { 618 m.retired[key] = sets.New[string](context) 619 } 620 return nil 621 } 622 623 func (m *fakeMigrator) migrate(org, repo, from, to string, _ func(string) bool) error { 624 key := orgRepo{org: org, repo: repo} 625 item := migration{from: from, to: to} 626 if contexts, exist := m.migrateErrors[key]; exist && contexts.has(item) { 627 return errors.New("failed to migrate context") 628 } 629 if _, exist := m.migrated[key]; exist { 630 m.migrated[key].insert(item) 631 } else { 632 newSet := migrationSet{} 633 newSet.insert(item) 634 m.migrated[key] = newSet 635 } 636 return nil 637 } 638 639 func newfakeProwJobTriggerer() fakeProwJobTriggerer { 640 return fakeProwJobTriggerer{ 641 errors: map[prKey]sets.Set[string]{}, 642 created: map[prKey]sets.Set[string]{}, 643 } 644 } 645 646 type prKey struct { 647 org, repo string 648 num int 649 } 650 651 type fakeProwJobTriggerer struct { 652 errors map[prKey]sets.Set[string] 653 created map[prKey]sets.Set[string] 654 } 655 656 func (c *fakeProwJobTriggerer) runAndSkip(pr *github.PullRequest, requestedJobs []config.Presubmit) error { 657 actions := []struct { 658 jobs []config.Presubmit 659 records map[prKey]sets.Set[string] 660 }{ 661 { 662 jobs: requestedJobs, 663 records: c.created, 664 }, 665 } 666 for _, action := range actions { 667 names := sets.New[string]() 668 key := prKey{org: pr.Base.Repo.Owner.Login, repo: pr.Base.Repo.Name, num: pr.Number} 669 for _, job := range action.jobs { 670 if jobErrors, exists := c.errors[key]; exists && jobErrors.Has(job.Name) { 671 return errors.New("failed to trigger prow job") 672 } 673 names.Insert(job.Name) 674 } 675 if current, exists := action.records[key]; exists { 676 action.records[key] = current.Union(names) 677 } else { 678 action.records[key] = names 679 } 680 } 681 return nil 682 } 683 684 func newFakeGitHubClient(key orgRepo) fakeGitHubClient { 685 return fakeGitHubClient{ 686 prErrors: orgRepoSet{}, 687 refErrors: map[orgRepo]sets.Set[string]{key: sets.New[string]()}, 688 prs: map[orgRepo][]github.PullRequest{key: {}}, 689 refs: map[orgRepo]map[string]string{key: {}}, 690 } 691 } 692 693 type fakeGitHubClient struct { 694 prErrors orgRepoSet 695 refErrors map[orgRepo]sets.Set[string] 696 changeErrors map[orgRepo]sets.Set[int] 697 698 prs map[orgRepo][]github.PullRequest 699 refs map[orgRepo]map[string]string 700 changes map[orgRepo]map[int][]github.PullRequestChange 701 } 702 703 func (c *fakeGitHubClient) GetPullRequests(org, repo string) ([]github.PullRequest, error) { 704 key := orgRepo{org: org, repo: repo} 705 if c.prErrors.has(key) { 706 return nil, errors.New("failed to get PRs") 707 } 708 return c.prs[key], nil 709 } 710 711 func (c *fakeGitHubClient) GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) { 712 key := orgRepo{org: org, repo: repo} 713 if changes, exist := c.changeErrors[key]; exist && changes.Has(number) { 714 return nil, errors.New("failed to get changes") 715 } 716 return c.changes[key][number], nil 717 } 718 719 func (c *fakeGitHubClient) GetRef(org, repo, ref string) (string, error) { 720 key := orgRepo{org: org, repo: repo} 721 if refs, exist := c.refErrors[key]; exist && refs.Has(ref) { 722 return "", errors.New("failed to get ref") 723 } 724 return c.refs[key][ref], nil 725 } 726 727 type prAuthor struct { 728 pr int 729 author string 730 } 731 732 type prAuthorSet map[prAuthor]interface{} 733 734 func (s prAuthorSet) has(item prAuthor) bool { 735 _, contained := s[item] 736 return contained 737 } 738 739 func newFakeTrustedChecker(key orgRepo) fakeTrustedChecker { 740 return fakeTrustedChecker{ 741 errors: map[orgRepo]prAuthorSet{key: {}}, 742 trusted: map[orgRepo]map[prAuthor]bool{key: {}}, 743 } 744 } 745 746 type fakeTrustedChecker struct { 747 errors map[orgRepo]prAuthorSet 748 749 trusted map[orgRepo]map[prAuthor]bool 750 } 751 752 func (c *fakeTrustedChecker) trustedPullRequest(author, org, repo string, num int) (bool, error) { 753 key := orgRepo{org: org, repo: repo} 754 item := prAuthor{pr: num, author: author} 755 if errs, exist := c.errors[key]; exist && errs.has(item) { 756 return false, errors.New("failed to check trusted") 757 } 758 return c.trusted[key][item], nil 759 } 760 761 func TestControllerReconcile(t *testing.T) { 762 // the diff from these configs causes: 763 // - deletion (required-job), 764 // - creation (new-required-job) 765 // - migration (other-required-job) 766 oldConfigData := `presubmits: 767 "org/repo": 768 - name: required-job 769 context: required-job 770 always_run: true 771 - name: other-required-job 772 context: other-required-job 773 always_run: true` 774 newConfigData := `presubmits: 775 "org/repo": 776 - name: other-required-job 777 context: new-context 778 always_run: true 779 - name: new-required-job 780 context: new-required-context 781 always_run: true 782 branches: 783 - base` 784 785 var oldConfig, newConfig config.Config 786 if err := yaml.Unmarshal([]byte(oldConfigData), &oldConfig); err != nil { 787 t.Fatalf("could not unmarshal old config: %v", err) 788 } 789 for _, presubmits := range oldConfig.PresubmitsStatic { 790 if err := config.SetPresubmitRegexes(presubmits); err != nil { 791 t.Fatalf("could not set presubmit regexes for old config: %v", err) 792 } 793 } 794 if err := yaml.Unmarshal([]byte(newConfigData), &newConfig); err != nil { 795 t.Fatalf("could not unmarshal new config: %v", err) 796 } 797 for _, presubmits := range newConfig.PresubmitsStatic { 798 if err := config.SetPresubmitRegexes(presubmits); err != nil { 799 t.Fatalf("could not set presubmit regexes for new config: %v", err) 800 } 801 } 802 delta := config.Delta{Before: oldConfig, After: newConfig} 803 migrate := migration{from: "other-required-job", to: "new-context"} 804 org, repo := "org", "repo" 805 orgRepoKey := orgRepo{org: org, repo: repo} 806 prNumber := 1 807 secondPrNumber := 2 808 thirdPrNumber := 3 809 author := "user" 810 prAuthorKey := prAuthor{author: author, pr: prNumber} 811 secondPrAuthorKey := prAuthor{author: author, pr: secondPrNumber} 812 thirdPrAuthorKey := prAuthor{author: author, pr: thirdPrNumber} 813 prOrgRepoKey := prKey{org: org, repo: repo, num: prNumber} 814 thirdPrOrgRepoKey := prKey{org: org, repo: repo, num: thirdPrNumber} 815 baseRef := "base" 816 otherBaseRef := "other" 817 baseSha := "abc" 818 notMergable := false 819 pr := github.PullRequest{ 820 User: github.User{ 821 Login: author, 822 }, 823 Number: prNumber, 824 Base: github.PullRequestBranch{ 825 Repo: github.Repo{ 826 Owner: github.User{ 827 Login: org, 828 }, 829 Name: repo, 830 }, 831 Ref: baseRef, 832 }, 833 Head: github.PullRequestBranch{ 834 SHA: "prsha", 835 }, 836 } 837 secondPr := github.PullRequest{ 838 User: github.User{ 839 Login: author, 840 }, 841 Number: secondPrNumber, 842 Base: github.PullRequestBranch{ 843 Repo: github.Repo{ 844 Owner: github.User{ 845 Login: org, 846 }, 847 Name: repo, 848 }, 849 Ref: baseRef, 850 }, 851 Head: github.PullRequestBranch{ 852 SHA: "prsha2", 853 }, 854 Mergable: ¬Mergable, 855 } 856 thirdPr := github.PullRequest{ 857 User: github.User{ 858 Login: author, 859 }, 860 Number: thirdPrNumber, 861 Base: github.PullRequestBranch{ 862 Repo: github.Repo{ 863 Owner: github.User{ 864 Login: org, 865 }, 866 Name: repo, 867 }, 868 Ref: otherBaseRef, 869 }, 870 Head: github.PullRequestBranch{ 871 SHA: "prsha3", 872 }, 873 } 874 var testCases = []struct { 875 name string 876 // generator creates the controller and a func that checks 877 // the internal state of the fakes in the controller 878 generator func() (Controller, func(*testing.T)) 879 expectErr bool 880 }{ 881 { 882 name: "ignored org skips creation, retire and migrate", 883 generator: func() (Controller, func(*testing.T)) { 884 fpjt := newfakeProwJobTriggerer() 885 fghc := newFakeGitHubClient(orgRepoKey) 886 fghc.prs[orgRepoKey] = []github.PullRequest{pr} 887 fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha 888 fsm := newFakeMigrator(orgRepoKey) 889 ftc := newFakeTrustedChecker(orgRepoKey) 890 ftc.trusted[orgRepoKey][prAuthorKey] = true 891 controller := Controller{ 892 continueOnError: true, 893 addedPresubmitDenylist: sets.New[string]("org"), 894 prowJobTriggerer: &fpjt, 895 githubClient: &fghc, 896 statusMigrator: &fsm, 897 trustedChecker: &ftc, 898 } 899 checker := func(t *testing.T) { 900 checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{}) 901 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}}) 902 } 903 return controller, checker 904 }, 905 }, 906 { 907 name: "ignored org/repo skips creation, retire and migrate", 908 generator: func() (Controller, func(*testing.T)) { 909 fpjt := newfakeProwJobTriggerer() 910 fghc := newFakeGitHubClient(orgRepoKey) 911 fghc.prs[orgRepoKey] = []github.PullRequest{pr} 912 fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha 913 fsm := newFakeMigrator(orgRepoKey) 914 ftc := newFakeTrustedChecker(orgRepoKey) 915 ftc.trusted[orgRepoKey][prAuthorKey] = true 916 controller := Controller{ 917 continueOnError: true, 918 addedPresubmitDenylist: sets.New[string]("org/repo"), 919 prowJobTriggerer: &fpjt, 920 githubClient: &fghc, 921 statusMigrator: &fsm, 922 trustedChecker: &ftc, 923 } 924 checker := func(t *testing.T) { 925 checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{}) 926 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}}) 927 } 928 return controller, checker 929 }, 930 }, 931 { 932 name: "ignored all org skips creation, retire and migrate", 933 generator: func() (Controller, func(*testing.T)) { 934 fpjt := newfakeProwJobTriggerer() 935 fghc := newFakeGitHubClient(orgRepoKey) 936 fghc.prs[orgRepoKey] = []github.PullRequest{pr} 937 fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha 938 fsm := newFakeMigrator(orgRepoKey) 939 ftc := newFakeTrustedChecker(orgRepoKey) 940 ftc.trusted[orgRepoKey][prAuthorKey] = true 941 controller := Controller{ 942 continueOnError: true, 943 addedPresubmitDenylistAll: sets.New[string]("org"), 944 prowJobTriggerer: &fpjt, 945 githubClient: &fghc, 946 statusMigrator: &fsm, 947 trustedChecker: &ftc, 948 } 949 checker := func(t *testing.T) { 950 checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{}) 951 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]()}, map[orgRepo]migrationSet{orgRepoKey: {}}) 952 } 953 return controller, checker 954 }, 955 }, 956 { 957 name: "ignored all org/repo skips creation, retire and migrate", 958 generator: func() (Controller, func(*testing.T)) { 959 fpjt := newfakeProwJobTriggerer() 960 fghc := newFakeGitHubClient(orgRepoKey) 961 fghc.prs[orgRepoKey] = []github.PullRequest{pr} 962 fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha 963 fsm := newFakeMigrator(orgRepoKey) 964 ftc := newFakeTrustedChecker(orgRepoKey) 965 ftc.trusted[orgRepoKey][prAuthorKey] = true 966 controller := Controller{ 967 continueOnError: true, 968 addedPresubmitDenylistAll: sets.New[string]("org/repo"), 969 prowJobTriggerer: &fpjt, 970 githubClient: &fghc, 971 statusMigrator: &fsm, 972 trustedChecker: &ftc, 973 } 974 checker := func(t *testing.T) { 975 checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{}) 976 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]()}, map[orgRepo]migrationSet{orgRepoKey: {}}) 977 } 978 return controller, checker 979 }, 980 }, 981 { 982 name: "no errors and trusted PR means we should see a trigger, retire and migrate", 983 generator: func() (Controller, func(*testing.T)) { 984 fpjt := newfakeProwJobTriggerer() 985 fghc := newFakeGitHubClient(orgRepoKey) 986 fghc.prs[orgRepoKey] = []github.PullRequest{pr} 987 fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha 988 fsm := newFakeMigrator(orgRepoKey) 989 ftc := newFakeTrustedChecker(orgRepoKey) 990 ftc.trusted[orgRepoKey][prAuthorKey] = true 991 controller := Controller{ 992 continueOnError: true, 993 addedPresubmitDenylist: sets.New[string](), 994 prowJobTriggerer: &fpjt, 995 githubClient: &fghc, 996 statusMigrator: &fsm, 997 trustedChecker: &ftc, 998 } 999 checker := func(t *testing.T) { 1000 expectedProwJob := map[prKey]sets.Set[string]{prOrgRepoKey: sets.New[string]("new-required-job")} 1001 checkTriggerer(t, fpjt, expectedProwJob) 1002 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}}) 1003 } 1004 return controller, checker 1005 }, 1006 }, 1007 { 1008 name: "no errors and untrusted PR means we should see no trigger, a retire and a migrate", 1009 generator: func() (Controller, func(*testing.T)) { 1010 fpjt := newfakeProwJobTriggerer() 1011 fghc := newFakeGitHubClient(orgRepoKey) 1012 fghc.prs[orgRepoKey] = []github.PullRequest{pr} 1013 fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha 1014 fsm := newFakeMigrator(orgRepoKey) 1015 ftc := newFakeTrustedChecker(orgRepoKey) 1016 ftc.trusted[orgRepoKey][prAuthorKey] = false 1017 controller := Controller{ 1018 continueOnError: true, 1019 addedPresubmitDenylist: sets.New[string](), 1020 prowJobTriggerer: &fpjt, 1021 githubClient: &fghc, 1022 statusMigrator: &fsm, 1023 trustedChecker: &ftc, 1024 } 1025 checker := func(t *testing.T) { 1026 checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{}) 1027 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}}) 1028 } 1029 return controller, checker 1030 }, 1031 }, 1032 { 1033 name: "no errors and unmergable PR means we should see no trigger, a retire and a migrate", 1034 generator: func() (Controller, func(*testing.T)) { 1035 fpjt := newfakeProwJobTriggerer() 1036 fghc := newFakeGitHubClient(orgRepoKey) 1037 fghc.prs[orgRepoKey] = []github.PullRequest{secondPr} 1038 fghc.refs[orgRepoKey]["heads/"+secondPr.Base.Ref] = baseSha 1039 fsm := newFakeMigrator(orgRepoKey) 1040 ftc := newFakeTrustedChecker(orgRepoKey) 1041 ftc.trusted[orgRepoKey][secondPrAuthorKey] = true 1042 controller := Controller{ 1043 continueOnError: true, 1044 addedPresubmitDenylist: sets.New[string](), 1045 prowJobTriggerer: &fpjt, 1046 githubClient: &fghc, 1047 statusMigrator: &fsm, 1048 trustedChecker: &ftc, 1049 } 1050 checker := func(t *testing.T) { 1051 checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{}) 1052 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}}) 1053 } 1054 return controller, checker 1055 }, 1056 }, 1057 { 1058 name: "no errors and PR that doesn't match the added job means we should see no trigger, a retire and a migrate", 1059 generator: func() (Controller, func(*testing.T)) { 1060 fpjt := newfakeProwJobTriggerer() 1061 fghc := newFakeGitHubClient(orgRepoKey) 1062 fghc.prs[orgRepoKey] = []github.PullRequest{thirdPr} 1063 fghc.refs[orgRepoKey]["heads/"+thirdPr.Base.Ref] = baseSha 1064 fsm := newFakeMigrator(orgRepoKey) 1065 ftc := newFakeTrustedChecker(orgRepoKey) 1066 ftc.trusted[orgRepoKey][thirdPrAuthorKey] = true 1067 controller := Controller{ 1068 continueOnError: true, 1069 addedPresubmitDenylist: sets.New[string](), 1070 prowJobTriggerer: &fpjt, 1071 githubClient: &fghc, 1072 statusMigrator: &fsm, 1073 trustedChecker: &ftc, 1074 } 1075 checker := func(t *testing.T) { 1076 checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{thirdPrOrgRepoKey: sets.New[string]()}) 1077 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}}) 1078 } 1079 return controller, checker 1080 }, 1081 }, 1082 { 1083 name: "trust check error means we should see no trigger, a retire and a migrate", 1084 generator: func() (Controller, func(*testing.T)) { 1085 fpjt := newfakeProwJobTriggerer() 1086 fghc := newFakeGitHubClient(orgRepoKey) 1087 fghc.prs[orgRepoKey] = []github.PullRequest{pr} 1088 fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha 1089 fsm := newFakeMigrator(orgRepoKey) 1090 ftc := newFakeTrustedChecker(orgRepoKey) 1091 ftc.errors = map[orgRepo]prAuthorSet{orgRepoKey: {prAuthorKey: nil}} 1092 controller := Controller{ 1093 continueOnError: true, 1094 addedPresubmitDenylist: sets.New[string](), 1095 prowJobTriggerer: &fpjt, 1096 githubClient: &fghc, 1097 statusMigrator: &fsm, 1098 trustedChecker: &ftc, 1099 } 1100 checker := func(t *testing.T) { 1101 checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{}) 1102 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}}) 1103 } 1104 return controller, checker 1105 }, 1106 expectErr: true, 1107 }, 1108 { 1109 name: "trigger error means we should see no trigger, a retire and a migrate", 1110 generator: func() (Controller, func(*testing.T)) { 1111 fpjt := newfakeProwJobTriggerer() 1112 fpjt.errors[prOrgRepoKey] = sets.New[string]("new-required-job") 1113 fghc := newFakeGitHubClient(orgRepoKey) 1114 fghc.prs[orgRepoKey] = []github.PullRequest{pr} 1115 fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha 1116 fsm := newFakeMigrator(orgRepoKey) 1117 ftc := newFakeTrustedChecker(orgRepoKey) 1118 ftc.errors = map[orgRepo]prAuthorSet{orgRepoKey: {prAuthorKey: nil}} 1119 controller := Controller{ 1120 continueOnError: true, 1121 addedPresubmitDenylist: sets.New[string](), 1122 prowJobTriggerer: &fpjt, 1123 githubClient: &fghc, 1124 statusMigrator: &fsm, 1125 trustedChecker: &ftc, 1126 } 1127 checker := func(t *testing.T) { 1128 checkTriggerer(t, fpjt, map[prKey]sets.Set[string]{}) 1129 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}}) 1130 } 1131 return controller, checker 1132 }, 1133 expectErr: true, 1134 }, 1135 { 1136 name: "retire errors and trusted PR means we should see a trigger and migrate", 1137 generator: func() (Controller, func(*testing.T)) { 1138 fpjt := newfakeProwJobTriggerer() 1139 fghc := newFakeGitHubClient(orgRepoKey) 1140 fghc.prs[orgRepoKey] = []github.PullRequest{pr} 1141 fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha 1142 fsm := newFakeMigrator(orgRepoKey) 1143 fsm.retireErrors = map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")} 1144 ftc := newFakeTrustedChecker(orgRepoKey) 1145 ftc.trusted[orgRepoKey][prAuthorKey] = true 1146 controller := Controller{ 1147 continueOnError: true, 1148 addedPresubmitDenylist: sets.New[string](), 1149 prowJobTriggerer: &fpjt, 1150 githubClient: &fghc, 1151 statusMigrator: &fsm, 1152 trustedChecker: &ftc, 1153 } 1154 checker := func(t *testing.T) { 1155 expectedProwJob := map[prKey]sets.Set[string]{prOrgRepoKey: sets.New[string]("new-required-job")} 1156 checkTriggerer(t, fpjt, expectedProwJob) 1157 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]()}, map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}}) 1158 } 1159 return controller, checker 1160 }, 1161 expectErr: true, 1162 }, 1163 { 1164 name: "migrate errors and trusted PR means we should see a trigger and retire", 1165 generator: func() (Controller, func(*testing.T)) { 1166 fpjt := newfakeProwJobTriggerer() 1167 fghc := newFakeGitHubClient(orgRepoKey) 1168 fghc.prs[orgRepoKey] = []github.PullRequest{pr} 1169 fghc.refs[orgRepoKey]["heads/"+pr.Base.Ref] = baseSha 1170 fsm := newFakeMigrator(orgRepoKey) 1171 fsm.migrateErrors = map[orgRepo]migrationSet{orgRepoKey: {migrate: nil}} 1172 ftc := newFakeTrustedChecker(orgRepoKey) 1173 ftc.trusted[orgRepoKey][prAuthorKey] = true 1174 controller := Controller{ 1175 continueOnError: true, 1176 addedPresubmitDenylist: sets.New[string](), 1177 prowJobTriggerer: &fpjt, 1178 githubClient: &fghc, 1179 statusMigrator: &fsm, 1180 trustedChecker: &ftc, 1181 } 1182 checker := func(t *testing.T) { 1183 expectedProwJob := map[prKey]sets.Set[string]{prOrgRepoKey: sets.New[string]("new-required-job")} 1184 checkTriggerer(t, fpjt, expectedProwJob) 1185 checkMigrator(t, fsm, map[orgRepo]sets.Set[string]{orgRepoKey: sets.New[string]("required-job")}, map[orgRepo]migrationSet{orgRepoKey: {}}) 1186 } 1187 return controller, checker 1188 }, 1189 expectErr: true, 1190 }, 1191 } 1192 1193 for _, testCase := range testCases { 1194 t.Run(testCase.name, func(t *testing.T) { 1195 controller, check := testCase.generator() 1196 err := controller.reconcile(delta, logrusEntry()) 1197 if err == nil && testCase.expectErr { 1198 t.Errorf("expected an error, but got none") 1199 } 1200 if err != nil && !testCase.expectErr { 1201 t.Errorf("expected no error, but got one: %v", err) 1202 } 1203 check(t) 1204 }) 1205 } 1206 } 1207 1208 func logrusEntry() *logrus.Entry { 1209 return logrus.NewEntry(logrus.StandardLogger()) 1210 } 1211 1212 func checkTriggerer(t *testing.T, triggerer fakeProwJobTriggerer, expectedCreatedJobs map[prKey]sets.Set[string]) { 1213 actual, expected := triggerer.created, expectedCreatedJobs 1214 if diff := cmp.Diff(actual, expected, ignoreUnexported); diff != "" { 1215 t.Errorf("did not create expected ProwJob: %s", diff) 1216 } 1217 } 1218 1219 func checkMigrator(t *testing.T, migrator fakeMigrator, expectedRetiredStatuses map[orgRepo]sets.Set[string], expectedMigratedStatuses map[orgRepo]migrationSet) { 1220 if diff := cmp.Diff(migrator.retired, expectedRetiredStatuses, ignoreUnexported); diff != "" { 1221 t.Errorf("did not retire correct statuses: %s", diff) 1222 } 1223 if diff := cmp.Diff(migrator.migrated, expectedMigratedStatuses, ignoreUnexported); diff != "" { 1224 t.Errorf("did not migrate correct statuses: %s", diff) 1225 } 1226 }