sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/tide/status_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 tide 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "sort" 24 "strings" 25 "testing" 26 27 "github.com/go-test/deep" 28 "github.com/google/go-cmp/cmp" 29 "github.com/google/go-cmp/cmp/cmpopts" 30 githubql "github.com/shurcooL/githubv4" 31 "github.com/sirupsen/logrus" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/runtime" 34 fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" 35 36 "k8s.io/apimachinery/pkg/util/sets" 37 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 38 "sigs.k8s.io/prow/pkg/config" 39 "sigs.k8s.io/prow/pkg/github" 40 "sigs.k8s.io/prow/pkg/tide/blockers" 41 ) 42 43 func TestExpectedStatus(t *testing.T) { 44 mergeLabel := "tide/merge-method-merge" 45 squashLabel := "tide/merge-method-squash" 46 neededLabelsWithAlt := []string{"need-1", "need-2", "need-a-very-super-duper-extra-not-short-at-all-label-name,need-3"} 47 neededLabels := []string{"need-1", "need-2", "need-a-very-super-duper-extra-not-short-at-all-label-name"} 48 forbiddenLabels := []string{"forbidden-1", "forbidden-2"} 49 testcases := []struct { 50 name string 51 52 baseref string 53 branchAllowList []string 54 branchDenyList []string 55 sameBranchReqs bool 56 labels []string 57 author string 58 firstQueryAuthor string 59 secondQueryAuthor string 60 milestone string 61 contexts []Context 62 checkRuns []CheckRun 63 inPool bool 64 blocks []int 65 prowJobs []runtime.Object 66 requiredContexts []string 67 mergeConflicts bool 68 displayAllTideQueries bool 69 additionalTideQueries []config.TideQuery 70 hasApprovingReview bool 71 singleQuery bool 72 73 state string 74 desc string 75 }{ 76 { 77 name: "in pool", 78 inPool: true, 79 80 state: github.StatusSuccess, 81 desc: statusInPool, 82 }, 83 { 84 name: "check truncation of label list", 85 author: "batman", 86 firstQueryAuthor: "batman", 87 secondQueryAuthor: "batman", 88 milestone: "v1.0", 89 inPool: false, 90 91 state: github.StatusPending, 92 desc: fmt.Sprintf(statusNotInPool, " Needs need-1, need-2 labels."), 93 }, 94 { 95 name: "check truncation of label list is not excessive", 96 labels: append([]string{}, neededLabels[:2]...), 97 author: "batman", 98 firstQueryAuthor: "batman", 99 secondQueryAuthor: "batman", 100 milestone: "v1.0", 101 inPool: false, 102 103 state: github.StatusPending, 104 desc: fmt.Sprintf(statusNotInPool, " Needs need-a-very-super-duper-extra-not-short-at-all-label-name or need-3 label."), 105 }, 106 { 107 name: "check multiple /tide labels result in conflict message", 108 labels: append(append([]string{}, neededLabels...), mergeLabel, squashLabel), 109 state: github.StatusError, 110 desc: fmt.Sprintf(statusNotInPool, " PR has conflicting merge method override labels"), 111 }, 112 { 113 name: "has forbidden labels", 114 labels: append(append([]string{}, neededLabels...), forbiddenLabels...), 115 author: "batman", 116 firstQueryAuthor: "batman", 117 secondQueryAuthor: "batman", 118 milestone: "v1.0", 119 inPool: false, 120 121 state: github.StatusPending, 122 desc: fmt.Sprintf(statusNotInPool, " Should not have forbidden-1, forbidden-2 labels."), 123 }, 124 { 125 name: "has one forbidden label", 126 labels: append(append([]string{}, neededLabels...), forbiddenLabels[0]), 127 author: "batman", 128 firstQueryAuthor: "batman", 129 secondQueryAuthor: "batman", 130 milestone: "v1.0", 131 inPool: false, 132 133 state: github.StatusPending, 134 desc: fmt.Sprintf(statusNotInPool, " Should not have forbidden-1 label."), 135 }, 136 { 137 name: "only mention one requirement class", 138 labels: append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]), 139 author: "batman", 140 firstQueryAuthor: "batman", 141 secondQueryAuthor: "batman", 142 milestone: "v1.0", 143 inPool: false, 144 145 state: github.StatusPending, 146 desc: fmt.Sprintf(statusNotInPool, " Needs need-1 label."), 147 }, 148 { 149 name: "mention all possible queries when opted in", 150 labels: append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]), 151 author: "batman", 152 firstQueryAuthor: "batman", 153 secondQueryAuthor: "batman", 154 milestone: "v1.0", 155 inPool: false, 156 displayAllTideQueries: true, 157 158 state: github.StatusPending, 159 desc: fmt.Sprintf(statusNotInPool, " Needs need-1 label OR Needs 1, 2, 3, 4, 5, 6, 7 labels."), 160 }, 161 { 162 name: "displayAllTideQueries but only one query", 163 labels: append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]), 164 author: "batman", 165 firstQueryAuthor: "batman", 166 secondQueryAuthor: "batman", 167 milestone: "v1.0", 168 inPool: false, 169 displayAllTideQueries: true, 170 singleQuery: true, 171 172 state: github.StatusPending, 173 desc: fmt.Sprintf(statusNotInPool, " Needs need-1 label."), 174 }, 175 { 176 name: "displayAllTideQueries when there is no matching query", 177 baseref: "bad", 178 branchDenyList: []string{"bad"}, 179 sameBranchReqs: true, 180 labels: append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]), 181 author: "batman", 182 firstQueryAuthor: "batman", 183 secondQueryAuthor: "batman", 184 milestone: "v1.0", 185 inPool: false, 186 displayAllTideQueries: true, 187 188 state: github.StatusPending, 189 desc: fmt.Sprintf(statusNotInPool, " No Tide query for branch bad found."), 190 }, 191 { 192 name: "displayAllTideQueries shows only queries matching the branch", 193 baseref: "main", 194 branchDenyList: []string{"main"}, 195 sameBranchReqs: true, 196 additionalTideQueries: []config.TideQuery{{ 197 Orgs: []string{""}, 198 Labels: []string{"good-to-go"}, 199 }}, 200 labels: append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]), 201 author: "batman", 202 firstQueryAuthor: "batman", 203 secondQueryAuthor: "batman", 204 milestone: "v1.0", 205 inPool: false, 206 displayAllTideQueries: true, 207 208 state: github.StatusPending, 209 desc: fmt.Sprintf(statusNotInPool, " Needs good-to-go label."), 210 }, 211 { 212 name: "against excluded branch", 213 baseref: "bad", 214 branchDenyList: []string{"bad"}, 215 sameBranchReqs: true, 216 labels: neededLabels, 217 inPool: false, 218 219 state: github.StatusPending, 220 desc: fmt.Sprintf(statusNotInPool, " Merging to branch bad is forbidden."), 221 }, 222 { 223 name: "not against included branch", 224 baseref: "bad", 225 branchAllowList: []string{"good"}, 226 sameBranchReqs: true, 227 labels: neededLabels, 228 inPool: false, 229 230 state: github.StatusPending, 231 desc: fmt.Sprintf(statusNotInPool, " Merging to branch bad is forbidden."), 232 }, 233 { 234 name: "choose query for correct branch", 235 baseref: "bad", 236 branchAllowList: []string{"good"}, 237 author: "batman", 238 firstQueryAuthor: "batman", 239 secondQueryAuthor: "batman", 240 milestone: "v1.0", 241 labels: neededLabels, 242 inPool: false, 243 244 state: github.StatusPending, 245 desc: fmt.Sprintf(statusNotInPool, " Needs 1, 2, 3, 4, 5, 6, 7 labels."), 246 }, 247 { 248 name: "tides own context failed but is ignored", 249 labels: neededLabels, 250 author: "batman", 251 firstQueryAuthor: "batman", 252 secondQueryAuthor: "batman", 253 milestone: "v1.0", 254 contexts: []Context{{Context: githubql.String(statusContext), State: githubql.StatusStateError}}, 255 inPool: false, 256 257 state: github.StatusSuccess, 258 desc: statusInPool, 259 }, 260 { 261 name: "single bad context", 262 labels: neededLabels, 263 contexts: []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateError}}, 264 author: "batman", 265 firstQueryAuthor: "batman", 266 secondQueryAuthor: "batman", 267 milestone: "v1.0", 268 inPool: false, 269 270 state: github.StatusPending, 271 desc: fmt.Sprintf(statusNotInPool, " Job job-name has not succeeded."), 272 }, 273 { 274 name: "single bad checkrun", 275 labels: neededLabels, 276 checkRuns: []CheckRun{{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)}}, 277 author: "batman", 278 firstQueryAuthor: "batman", 279 secondQueryAuthor: "batman", 280 milestone: "v1.0", 281 inPool: false, 282 283 state: github.StatusPending, 284 desc: fmt.Sprintf(statusNotInPool, " Job job-name has not succeeded."), 285 }, 286 { 287 name: "single good checkrun", 288 labels: neededLabels, 289 checkRuns: []CheckRun{{Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)}}, 290 author: "batman", 291 firstQueryAuthor: "batman", 292 secondQueryAuthor: "batman", 293 milestone: "v1.0", 294 inPool: true, 295 296 state: github.StatusSuccess, 297 desc: statusInPool, 298 }, 299 { 300 name: "multiple good checkruns", 301 labels: neededLabels, 302 checkRuns: []CheckRun{ 303 {Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)}, 304 {Name: githubql.String("another-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)}, 305 }, 306 author: "batman", 307 firstQueryAuthor: "batman", 308 secondQueryAuthor: "batman", 309 milestone: "v1.0", 310 inPool: true, 311 312 state: github.StatusSuccess, 313 desc: statusInPool, 314 }, 315 { 316 name: "mix of good and bad checkruns", 317 labels: neededLabels, 318 checkRuns: []CheckRun{ 319 {Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)}, 320 {Name: githubql.String("another-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)}, 321 }, 322 author: "batman", 323 firstQueryAuthor: "batman", 324 secondQueryAuthor: "batman", 325 milestone: "v1.0", 326 inPool: false, 327 328 state: github.StatusPending, 329 desc: fmt.Sprintf(statusNotInPool, " Job another-job has not succeeded."), 330 }, 331 { 332 name: "mix of good status contexts and checkruns", 333 labels: neededLabels, 334 checkRuns: []CheckRun{ 335 {Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)}, 336 }, 337 contexts: []Context{ 338 {Context: githubql.String("other-job-name"), State: githubql.StatusStateSuccess}, 339 }, 340 author: "batman", 341 firstQueryAuthor: "batman", 342 secondQueryAuthor: "batman", 343 milestone: "v1.0", 344 inPool: true, 345 346 state: github.StatusSuccess, 347 desc: statusInPool, 348 }, 349 { 350 name: "mix of bad status contexts and checkruns", 351 labels: neededLabels, 352 checkRuns: []CheckRun{ 353 {Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)}, 354 }, 355 contexts: []Context{ 356 {Context: githubql.String("other-job-name"), State: githubql.StatusStateFailure}, 357 }, 358 author: "batman", 359 firstQueryAuthor: "batman", 360 secondQueryAuthor: "batman", 361 milestone: "v1.0", 362 inPool: false, 363 364 state: github.StatusPending, 365 desc: fmt.Sprintf(statusNotInPool, " Jobs job-name, other-job-name have not succeeded."), 366 }, 367 { 368 name: "good context, bad checkrun", 369 labels: neededLabels, 370 checkRuns: []CheckRun{ 371 {Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)}, 372 }, 373 contexts: []Context{ 374 {Context: githubql.String("other-job-name"), State: githubql.StatusStateSuccess}, 375 }, 376 author: "batman", 377 firstQueryAuthor: "batman", 378 secondQueryAuthor: "batman", 379 milestone: "v1.0", 380 inPool: false, 381 382 state: github.StatusPending, 383 desc: fmt.Sprintf(statusNotInPool, " Job job-name has not succeeded."), 384 }, 385 { 386 name: "bad context, good checkrun", 387 labels: neededLabels, 388 checkRuns: []CheckRun{ 389 {Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)}, 390 }, 391 contexts: []Context{ 392 {Context: githubql.String("other-job-name"), State: githubql.StatusStateFailure}, 393 }, 394 author: "batman", 395 firstQueryAuthor: "batman", 396 secondQueryAuthor: "batman", 397 milestone: "v1.0", 398 inPool: false, 399 400 state: github.StatusPending, 401 desc: fmt.Sprintf(statusNotInPool, " Job other-job-name has not succeeded."), 402 }, 403 { 404 name: "multiple bad contexts", 405 labels: neededLabels, 406 author: "batman", 407 firstQueryAuthor: "batman", 408 secondQueryAuthor: "batman", 409 milestone: "v1.0", 410 contexts: []Context{ 411 {Context: githubql.String("job-name"), State: githubql.StatusStateError}, 412 {Context: githubql.String("other-job-name"), State: githubql.StatusStateError}, 413 }, 414 inPool: false, 415 416 state: github.StatusPending, 417 desc: fmt.Sprintf(statusNotInPool, " Jobs job-name, other-job-name have not succeeded."), 418 }, 419 { 420 name: "multiple bad checkruns", 421 labels: neededLabels, 422 author: "batman", 423 firstQueryAuthor: "batman", 424 secondQueryAuthor: "batman", 425 milestone: "v1.0", 426 checkRuns: []CheckRun{ 427 {Name: githubql.String("job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)}, 428 {Name: githubql.String("other-job-name"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateFailure)}, 429 }, 430 inPool: false, 431 432 state: github.StatusPending, 433 desc: fmt.Sprintf(statusNotInPool, " Jobs job-name, other-job-name have not succeeded."), 434 }, 435 { 436 name: "wrong author", 437 labels: neededLabels, 438 author: "robin", 439 firstQueryAuthor: "batman", 440 secondQueryAuthor: "batman", 441 milestone: "v1.0", 442 contexts: []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}}, 443 inPool: false, 444 445 state: github.StatusPending, 446 desc: fmt.Sprintf(statusNotInPool, " Must be by author batman."), 447 }, 448 { 449 name: "wrong author; use lowest diff", 450 labels: neededLabels, 451 author: "robin", 452 firstQueryAuthor: "penguin", 453 secondQueryAuthor: "batman", 454 milestone: "v1.0", 455 contexts: []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}}, 456 inPool: false, 457 458 state: github.StatusPending, 459 desc: fmt.Sprintf(statusNotInPool, " Must be by author penguin."), 460 }, 461 { 462 name: "wrong milestone", 463 labels: neededLabels, 464 author: "batman", 465 firstQueryAuthor: "batman", 466 secondQueryAuthor: "batman", 467 milestone: "v1.1", 468 contexts: []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}}, 469 inPool: false, 470 471 state: github.StatusPending, 472 desc: fmt.Sprintf(statusNotInPool, " Must be in milestone v1.0."), 473 }, 474 { 475 name: "not in pool, but all requirements are met", 476 labels: neededLabels, 477 author: "batman", 478 firstQueryAuthor: "batman", 479 secondQueryAuthor: "batman", 480 milestone: "v1.0", 481 inPool: false, 482 483 state: github.StatusSuccess, 484 desc: statusInPool, 485 }, 486 { 487 name: "not in pool, but all requirements are met, including a successful third-party context", 488 labels: neededLabels, 489 author: "batman", 490 firstQueryAuthor: "batman", 491 secondQueryAuthor: "batman", 492 milestone: "v1.0", 493 contexts: []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}}, 494 inPool: false, 495 496 state: github.StatusSuccess, 497 desc: statusInPool, 498 }, 499 { 500 name: "check that min diff query is used", 501 labels: []string{"3", "4", "5", "6", "7"}, 502 author: "batman", 503 firstQueryAuthor: "batman", 504 secondQueryAuthor: "batman", 505 milestone: "v1.0", 506 inPool: false, 507 508 state: github.StatusPending, 509 desc: fmt.Sprintf(statusNotInPool, " Needs 1, 2 labels."), 510 }, 511 { 512 name: "check that blockers take precedence over other queries", 513 labels: []string{"3", "4", "5", "6", "7"}, 514 author: "batman", 515 firstQueryAuthor: "batman", 516 secondQueryAuthor: "batman", 517 milestone: "v1.0", 518 inPool: false, 519 blocks: []int{1, 2}, 520 521 state: github.StatusError, 522 desc: fmt.Sprintf(statusNotInPool, " Merging is blocked by issues 1, 2."), 523 }, 524 { 525 name: "missing passing up-to-date context", 526 inPool: true, 527 baseref: "baseref", 528 requiredContexts: []string{"foo", "bar"}, 529 prowJobs: []runtime.Object{ 530 &prowapi.ProwJob{ 531 ObjectMeta: metav1.ObjectMeta{Name: "123"}, 532 Spec: prowapi.ProwJobSpec{ 533 Context: "foo", 534 Refs: &prowapi.Refs{ 535 BaseSHA: "baseref", 536 Pulls: []prowapi.Pull{{SHA: "head"}}, 537 }, 538 Type: prowapi.PresubmitJob, 539 }, 540 Status: prowapi.ProwJobStatus{ 541 State: prowapi.SuccessState, 542 }, 543 }, 544 &prowapi.ProwJob{ 545 ObjectMeta: metav1.ObjectMeta{Name: "1234"}, 546 Spec: prowapi.ProwJobSpec{ 547 Context: "bar", 548 Refs: &prowapi.Refs{ 549 BaseSHA: "baseref", 550 Pulls: []prowapi.Pull{{SHA: "head"}}, 551 }, 552 Type: prowapi.PresubmitJob, 553 }, 554 Status: prowapi.ProwJobStatus{ 555 State: prowapi.PendingState, 556 }, 557 }, 558 }, 559 560 state: github.StatusPending, 561 desc: "Not mergeable. Retesting: bar", 562 }, 563 { 564 name: "missing passing up-to-date contexts", 565 inPool: true, 566 baseref: "baseref", 567 requiredContexts: []string{"foo", "bar", "baz"}, 568 prowJobs: []runtime.Object{ 569 &prowapi.ProwJob{ 570 ObjectMeta: metav1.ObjectMeta{Name: "123"}, 571 Spec: prowapi.ProwJobSpec{ 572 Context: "foo", 573 Refs: &prowapi.Refs{ 574 BaseSHA: "baseref", 575 Pulls: []prowapi.Pull{{SHA: "head"}}, 576 }, 577 Type: prowapi.PresubmitJob, 578 }, 579 Status: prowapi.ProwJobStatus{ 580 State: prowapi.SuccessState, 581 }, 582 }, 583 &prowapi.ProwJob{ 584 ObjectMeta: metav1.ObjectMeta{Name: "1234"}, 585 Spec: prowapi.ProwJobSpec{ 586 Context: "bar", 587 Refs: &prowapi.Refs{ 588 BaseSHA: "baseref", 589 Pulls: []prowapi.Pull{{SHA: "head"}}, 590 }, 591 Type: prowapi.PresubmitJob, 592 }, 593 Status: prowapi.ProwJobStatus{ 594 State: prowapi.PendingState, 595 }, 596 }, 597 &prowapi.ProwJob{ 598 ObjectMeta: metav1.ObjectMeta{Name: "12345"}, 599 Spec: prowapi.ProwJobSpec{ 600 Context: "baz", 601 Refs: &prowapi.Refs{ 602 BaseSHA: "baseref", 603 Pulls: []prowapi.Pull{{SHA: "head"}}, 604 }, 605 Type: prowapi.PresubmitJob, 606 }, 607 Status: prowapi.ProwJobStatus{ 608 State: prowapi.PendingState, 609 }, 610 }, 611 }, 612 613 state: github.StatusPending, 614 desc: "Not mergeable. Retesting: bar baz", 615 }, 616 { 617 name: "missing passing up-to-date contexts with different ordering", 618 inPool: true, 619 baseref: "baseref", 620 requiredContexts: []string{"foo", "bar", "baz"}, 621 prowJobs: []runtime.Object{ 622 &prowapi.ProwJob{ 623 ObjectMeta: metav1.ObjectMeta{Name: "123"}, 624 Spec: prowapi.ProwJobSpec{ 625 Context: "foo", 626 Refs: &prowapi.Refs{ 627 BaseSHA: "baseref", 628 Pulls: []prowapi.Pull{{SHA: "head"}}, 629 }, 630 Type: prowapi.PresubmitJob, 631 }, 632 Status: prowapi.ProwJobStatus{ 633 State: prowapi.SuccessState, 634 }, 635 }, 636 &prowapi.ProwJob{ 637 ObjectMeta: metav1.ObjectMeta{Name: "1234"}, 638 Spec: prowapi.ProwJobSpec{ 639 Context: "baz", 640 Refs: &prowapi.Refs{ 641 BaseSHA: "baseref", 642 Pulls: []prowapi.Pull{{SHA: "head"}}, 643 }, 644 Type: prowapi.PresubmitJob, 645 }, 646 Status: prowapi.ProwJobStatus{ 647 State: prowapi.PendingState, 648 }, 649 }, 650 &prowapi.ProwJob{ 651 ObjectMeta: metav1.ObjectMeta{Name: "12345"}, 652 Spec: prowapi.ProwJobSpec{ 653 Context: "bar", 654 Refs: &prowapi.Refs{ 655 BaseSHA: "baseref", 656 Pulls: []prowapi.Pull{{SHA: "head"}}, 657 }, 658 Type: prowapi.PresubmitJob, 659 }, 660 Status: prowapi.ProwJobStatus{ 661 State: prowapi.PendingState, 662 }, 663 }, 664 }, 665 666 state: github.StatusPending, 667 desc: "Not mergeable. Retesting: bar baz", 668 }, 669 { 670 name: "long list of not up-to-date contexts results in shortened message", 671 inPool: true, 672 baseref: "baseref", 673 requiredContexts: []string{ 674 strings.Repeat("very-long-context", 8), 675 strings.Repeat("also-long-content", 8), 676 }, 677 prowJobs: []runtime.Object{ 678 &prowapi.ProwJob{ 679 ObjectMeta: metav1.ObjectMeta{Name: "123"}, 680 Spec: prowapi.ProwJobSpec{ 681 Context: strings.Repeat("very-long-context", 8), 682 Refs: &prowapi.Refs{ 683 BaseSHA: "baseref", 684 Pulls: []prowapi.Pull{{SHA: "head"}}, 685 }, 686 Type: prowapi.PresubmitJob, 687 }, 688 Status: prowapi.ProwJobStatus{ 689 State: prowapi.PendingState, 690 }, 691 }, 692 &prowapi.ProwJob{ 693 ObjectMeta: metav1.ObjectMeta{Name: "1234"}, 694 Spec: prowapi.ProwJobSpec{ 695 Context: strings.Repeat("also-long-content", 8), 696 Refs: &prowapi.Refs{ 697 BaseSHA: "baseref", 698 Pulls: []prowapi.Pull{{SHA: "head"}}, 699 }, 700 Type: prowapi.PresubmitJob, 701 }, 702 Status: prowapi.ProwJobStatus{ 703 State: prowapi.PendingState, 704 }, 705 }, 706 }, 707 708 state: github.StatusPending, 709 desc: "Not mergeable. Retesting 2 jobs.", 710 }, 711 { 712 name: "mergeconflicts", 713 inPool: true, 714 mergeConflicts: true, 715 state: github.StatusError, 716 desc: "Not mergeable. PR has a merge conflict.", 717 }, 718 { 719 name: "Missing approving review", 720 additionalTideQueries: []config.TideQuery{{Orgs: []string{""}, ReviewApprovedRequired: true}}, 721 inPool: false, 722 723 state: github.StatusPending, 724 desc: "Not mergeable. PullRequest is missing sufficient approving GitHub review(s)", 725 }, 726 { 727 name: "Required approving review is present", 728 additionalTideQueries: []config.TideQuery{{Orgs: []string{""}, ReviewApprovedRequired: true}}, 729 inPool: false, 730 hasApprovingReview: true, 731 732 state: github.StatusSuccess, 733 desc: "In merge pool.", 734 }, 735 } 736 737 for _, tc := range testcases { 738 t.Run(tc.name, func(t *testing.T) { 739 secondQuery := config.TideQuery{ 740 Orgs: []string{""}, 741 Labels: []string{"1", "2", "3", "4", "5", "6", "7"}, // lots of requirements 742 Author: tc.secondQueryAuthor, 743 Milestone: "v1.0", 744 } 745 if tc.sameBranchReqs { 746 secondQuery.ExcludedBranches = tc.branchDenyList 747 secondQuery.IncludedBranches = tc.branchAllowList 748 } 749 queries := config.TideQueries{ 750 config.TideQuery{ 751 Orgs: []string{""}, 752 ExcludedBranches: tc.branchDenyList, 753 IncludedBranches: tc.branchAllowList, 754 Labels: neededLabelsWithAlt, 755 MissingLabels: forbiddenLabels, 756 Author: tc.firstQueryAuthor, 757 Milestone: "v1.0", 758 }, 759 secondQuery, 760 } 761 if tc.singleQuery { 762 queries = config.TideQueries{queries[0]} 763 } 764 queries = append(queries, tc.additionalTideQueries...) 765 queriesByRepo := queries.QueryMap() 766 var pr PullRequest 767 pr.BaseRef = struct { 768 Name githubql.String 769 Prefix githubql.String 770 }{ 771 Name: githubql.String(tc.baseref), 772 } 773 for _, label := range tc.labels { 774 pr.Labels.Nodes = append( 775 pr.Labels.Nodes, 776 struct{ Name githubql.String }{Name: githubql.String(label)}, 777 ) 778 } 779 pr.HeadRefOID = githubql.String("head") 780 var checkRunNodes []CheckRunNode 781 for _, checkRun := range tc.checkRuns { 782 checkRunNodes = append(checkRunNodes, CheckRunNode{CheckRun: checkRun}) 783 } 784 pr.Commits.Nodes = append( 785 pr.Commits.Nodes, 786 struct{ Commit Commit }{ 787 Commit: Commit{ 788 Status: struct{ Contexts []Context }{ 789 Contexts: tc.contexts, 790 }, 791 OID: githubql.String("head"), 792 StatusCheckRollup: StatusCheckRollup{ 793 Contexts: StatusCheckRollupContext{ 794 Nodes: checkRunNodes, 795 }, 796 }, 797 }, 798 }, 799 ) 800 pr.Author = struct { 801 Login githubql.String 802 }{githubql.String(tc.author)} 803 if tc.milestone != "" { 804 pr.Milestone = &Milestone{githubql.String(tc.milestone)} 805 } 806 if tc.mergeConflicts { 807 pr.Mergeable = githubql.MergeableStateConflicting 808 } 809 if tc.hasApprovingReview { 810 pr.ReviewDecision = githubql.PullRequestReviewDecisionApproved 811 } 812 var pool map[string]CodeReviewCommon 813 if tc.inPool { 814 pool = map[string]CodeReviewCommon{"#0": {}} 815 } 816 blocks := blockers.Blockers{ 817 Repo: map[blockers.OrgRepo][]blockers.Blocker{}, 818 } 819 var items []blockers.Blocker 820 for _, block := range tc.blocks { 821 items = append(items, blockers.Blocker{Number: block}) 822 } 823 blocks.Repo[blockers.OrgRepo{Org: "", Repo: ""}] = items 824 825 ca := &config.Agent{} 826 ca.Set(&config.Config{ProwConfig: config.ProwConfig{Tide: config.Tide{ 827 TideGitHubConfig: config.TideGitHubConfig{ 828 DisplayAllQueriesInStatus: tc.displayAllTideQueries, 829 MergeLabel: mergeLabel, 830 SquashLabel: squashLabel, 831 }}}}) 832 mmc := newMergeChecker(ca.Config, &fgc{}) 833 834 sc, err := newStatusController( 835 context.Background(), 836 logrus.NewEntry(logrus.StandardLogger()), 837 nil, 838 newFakeManager(tc.prowJobs...), 839 nil, 840 ca.Config, 841 nil, 842 "", 843 mmc, 844 false, 845 &statusUpdate{ 846 dontUpdateStatus: &threadSafePRSet{}, 847 newPoolPending: make(chan bool), 848 }, 849 ) 850 if err != nil { 851 t.Fatalf("failed to get statusController: %v", err) 852 } 853 ccg := func() (contextChecker, error) { 854 return &config.TideContextPolicy{RequiredContexts: tc.requiredContexts}, nil 855 } 856 state, desc, err := sc.expectedStatus(sc.logger, queriesByRepo, CodeReviewCommonFromPullRequest(&pr), pool, ccg, blocks, tc.baseref) 857 if err != nil { 858 t.Fatalf("error calling expectedStatus(): %v", err) 859 } 860 if state != tc.state { 861 t.Errorf("Expected status state %q, but got %q.", string(tc.state), string(state)) 862 } 863 if desc != tc.desc { 864 t.Errorf("Expected status description %q, but got %q.", tc.desc, desc) 865 } 866 }) 867 } 868 } 869 870 func TestSetStatuses(t *testing.T) { 871 statusNotInPoolEmpty := fmt.Sprintf(statusNotInPool, "") 872 testcases := []struct { 873 name string 874 875 inPool bool 876 hasContext bool 877 inDontSetStatus bool 878 state githubql.StatusState 879 desc string 880 881 shouldSet bool 882 }{ 883 { 884 name: "in pool with proper context", 885 886 inPool: true, 887 hasContext: true, 888 state: githubql.StatusStateSuccess, 889 desc: statusInPool, 890 891 shouldSet: false, 892 }, 893 { 894 name: "in pool without context", 895 896 inPool: true, 897 hasContext: false, 898 899 shouldSet: true, 900 }, 901 { 902 name: "in pool with improper context", 903 904 inPool: true, 905 hasContext: true, 906 state: githubql.StatusStateSuccess, 907 desc: statusNotInPoolEmpty, 908 909 shouldSet: true, 910 }, 911 { 912 name: "in pool with wrong state", 913 914 inPool: true, 915 hasContext: true, 916 state: githubql.StatusStatePending, 917 desc: statusInPool, 918 919 shouldSet: true, 920 }, 921 { 922 name: "in pool with wrong state but set to not update status", 923 924 inPool: true, 925 hasContext: true, 926 inDontSetStatus: true, 927 state: githubql.StatusStatePending, 928 desc: statusInPool, 929 930 shouldSet: false, 931 }, 932 { 933 name: "not in pool with proper context", 934 935 inPool: false, 936 hasContext: true, 937 state: githubql.StatusStatePending, 938 desc: statusNotInPoolEmpty, 939 940 shouldSet: false, 941 }, 942 { 943 name: "not in pool with improper context", 944 945 inPool: false, 946 hasContext: true, 947 state: githubql.StatusStatePending, 948 desc: statusInPool, 949 950 shouldSet: true, 951 }, 952 { 953 name: "not in pool with no context", 954 955 inPool: false, 956 hasContext: false, 957 958 shouldSet: true, 959 }, 960 } 961 for _, tc := range testcases { 962 var pr PullRequest 963 pr.Commits.Nodes = []struct{ Commit Commit }{{}} 964 if tc.hasContext { 965 pr.Commits.Nodes[0].Commit.Status.Contexts = []Context{ 966 { 967 Context: githubql.String(statusContext), 968 State: tc.state, 969 Description: githubql.String(tc.desc), 970 }, 971 } 972 } 973 crc := CodeReviewCommonFromPullRequest(&pr) 974 pool := make(map[string]CodeReviewCommon) 975 if tc.inPool { 976 pool[prKey(crc)] = *crc 977 } 978 fc := &fgc{ 979 refs: map[string]string{"/ heads/": "SHA"}, 980 } 981 ca := &config.Agent{} 982 ca.Set(&config.Config{}) 983 // setStatuses logs instead of returning errors. 984 // Construct a logger to watch for errors to be printed. 985 log := logrus.WithField("component", "tide") 986 initialLog, err := log.String() 987 if err != nil { 988 t.Fatalf("Failed to get log output before testing: %v", err) 989 } 990 991 mmc := newMergeChecker(ca.Config, fc) 992 sc, err := newStatusController( 993 context.Background(), 994 log, 995 fc, 996 newFakeManager(), 997 nil, 998 ca.Config, 999 nil, 1000 "", 1001 mmc, 1002 false, 1003 &statusUpdate{ 1004 dontUpdateStatus: &threadSafePRSet{}, 1005 newPoolPending: make(chan bool), 1006 }, 1007 ) 1008 if err != nil { 1009 t.Fatalf("failed to get statusController: %v", err) 1010 } 1011 if tc.inDontSetStatus { 1012 sc.dontUpdateStatus = &threadSafePRSet{data: map[pullRequestIdentifier]struct{}{{}: {}}} 1013 } 1014 sc.setStatuses([]CodeReviewCommon{*crc}, pool, blockers.Blockers{}, nil, nil) 1015 if str, err := log.String(); err != nil { 1016 t.Fatalf("For case %s: failed to get log output: %v", tc.name, err) 1017 } else if str != initialLog { 1018 t.Errorf("For case %s: error setting status: %s", tc.name, str) 1019 } 1020 if tc.shouldSet && !fc.setStatus { 1021 t.Errorf("For case %s: should set but didn't", tc.name) 1022 } else if !tc.shouldSet && fc.setStatus { 1023 t.Errorf("For case %s: should not set but did", tc.name) 1024 } 1025 } 1026 } 1027 1028 func TestTargetUrl(t *testing.T) { 1029 testcases := []struct { 1030 name string 1031 pr *PullRequest 1032 config config.Tide 1033 1034 expectedURL string 1035 }{ 1036 { 1037 name: "no config", 1038 pr: &PullRequest{}, 1039 config: config.Tide{}, 1040 expectedURL: "", 1041 }, 1042 { 1043 name: "tide overview config", 1044 pr: &PullRequest{}, 1045 config: config.Tide{TideGitHubConfig: config.TideGitHubConfig{TargetURLs: map[string]string{"*": "tide.com"}}}, 1046 expectedURL: "tide.com", 1047 }, 1048 { 1049 name: "PR dashboard config and overview config", 1050 pr: &PullRequest{}, 1051 config: config.Tide{TideGitHubConfig: config.TideGitHubConfig{TargetURLs: map[string]string{"*": "tide.com"}, PRStatusBaseURLs: map[string]string{"*": "pr.status.com"}}}, 1052 expectedURL: "tide.com", 1053 }, 1054 { 1055 name: "PR dashboard config", 1056 pr: &PullRequest{ 1057 Author: struct { 1058 Login githubql.String 1059 }{Login: githubql.String("author")}, 1060 Repository: struct { 1061 Name githubql.String 1062 NameWithOwner githubql.String 1063 Owner struct { 1064 Login githubql.String 1065 } 1066 }{NameWithOwner: githubql.String("org/repo")}, 1067 HeadRefName: "head", 1068 }, 1069 config: config.Tide{TideGitHubConfig: config.TideGitHubConfig{PRStatusBaseURLs: map[string]string{"*": "pr.status.com"}}}, 1070 expectedURL: "pr.status.com?query=is%3Apr+repo%3Aorg%2Frepo+author%3Aauthor+head%3Ahead", 1071 }, 1072 { 1073 name: "generate link by default config", 1074 pr: &PullRequest{ 1075 Author: struct { 1076 Login githubql.String 1077 }{Login: githubql.String("author")}, 1078 Repository: struct { 1079 Name githubql.String 1080 NameWithOwner githubql.String 1081 Owner struct { 1082 Login githubql.String 1083 } 1084 }{ 1085 Owner: struct{ Login githubql.String }{Login: githubql.String("testOrg")}, 1086 Name: githubql.String("testRepo"), 1087 NameWithOwner: githubql.String("testOrg/testRepo"), 1088 }, 1089 HeadRefName: "head", 1090 }, 1091 config: config.Tide{TideGitHubConfig: config.TideGitHubConfig{PRStatusBaseURLs: map[string]string{"*": "default.pr.status.com"}}}, 1092 expectedURL: "default.pr.status.com?query=is%3Apr+repo%3AtestOrg%2FtestRepo+author%3Aauthor+head%3Ahead", 1093 }, 1094 { 1095 name: "generate link by org config", 1096 pr: &PullRequest{ 1097 Author: struct { 1098 Login githubql.String 1099 }{Login: githubql.String("author")}, 1100 Repository: struct { 1101 Name githubql.String 1102 NameWithOwner githubql.String 1103 Owner struct { 1104 Login githubql.String 1105 } 1106 }{ 1107 Owner: struct{ Login githubql.String }{Login: githubql.String("testOrg")}, 1108 Name: githubql.String("testRepo"), 1109 NameWithOwner: githubql.String("testOrg/testRepo"), 1110 }, 1111 HeadRefName: "head", 1112 }, 1113 config: config.Tide{TideGitHubConfig: config.TideGitHubConfig{PRStatusBaseURLs: map[string]string{ 1114 "*": "default.pr.status.com", 1115 "testOrg": "byorg.pr.status.com"}, 1116 }}, 1117 expectedURL: "byorg.pr.status.com?query=is%3Apr+repo%3AtestOrg%2FtestRepo+author%3Aauthor+head%3Ahead", 1118 }, 1119 { 1120 name: "generate link by repo config", 1121 pr: &PullRequest{ 1122 Author: struct { 1123 Login githubql.String 1124 }{Login: githubql.String("author")}, 1125 Repository: struct { 1126 Name githubql.String 1127 NameWithOwner githubql.String 1128 Owner struct { 1129 Login githubql.String 1130 } 1131 }{ 1132 Owner: struct{ Login githubql.String }{Login: githubql.String("testOrg")}, 1133 Name: githubql.String("testRepo"), 1134 NameWithOwner: githubql.String("testOrg/testRepo"), 1135 }, 1136 HeadRefName: "head", 1137 }, 1138 config: config.Tide{TideGitHubConfig: config.TideGitHubConfig{PRStatusBaseURLs: map[string]string{ 1139 "*": "default.pr.status.com", 1140 "testOrg": "byorg.pr.status.com", 1141 "testOrg/testRepo": "byrepo.pr.status.com"}, 1142 }}, 1143 expectedURL: "byrepo.pr.status.com?query=is%3Apr+repo%3AtestOrg%2FtestRepo+author%3Aauthor+head%3Ahead", 1144 }, 1145 } 1146 1147 for _, tc := range testcases { 1148 log := logrus.WithField("controller", "status-update") 1149 c := &config.Config{ProwConfig: config.ProwConfig{Tide: tc.config}} 1150 if actual, expected := targetURL(c, CodeReviewCommonFromPullRequest(tc.pr), log), tc.expectedURL; actual != expected { 1151 t.Errorf("%s: expected target URL %s but got %s", tc.name, expected, actual) 1152 } 1153 } 1154 } 1155 1156 func TestOpenPRsQuery(t *testing.T) { 1157 orgs := []string{"org", "kuber"} 1158 repos := []string{"k8s/k8s", "k8s/t-i"} 1159 exceptions := map[string]sets.Set[string]{ 1160 "org": sets.New[string]("org/repo1", "org/repo2"), 1161 "irrelevant-org": sets.New[string]("irrelevant-org/repo1", "irrelevant-org/repo2"), 1162 } 1163 1164 queriesByOrg := openPRsQueries(orgs, repos, exceptions) 1165 expectedQueriesByOrg := map[string]string{ 1166 "org": `-repo:"org/repo1" -repo:"org/repo2" archived:false is:pr org:"org" sort:updated-asc state:open`, 1167 "kuber": `archived:false is:pr org:"kuber" sort:updated-asc state:open`, 1168 "k8s": ` archived:false is:pr repo:"k8s/k8s" repo:"k8s/t-i" sort:updated-asc state:open`, 1169 } 1170 for org, query := range queriesByOrg { 1171 // This is produced from a map so the result is not deterministic. Work around by using 1172 // the fact that the parameters are space split and do a space split, sort, space join. 1173 split := strings.Split(query, " ") 1174 sort.Strings(split) 1175 queriesByOrg[org] = strings.Join(split, " ") 1176 } 1177 1178 if diff := cmp.Diff(queriesByOrg, expectedQueriesByOrg); diff != "" { 1179 t.Errorf("actual queries differ from expected: %s", diff) 1180 } 1181 } 1182 1183 func TestIndexFuncPassingJobs(t *testing.T) { 1184 testCases := []struct { 1185 name string 1186 pj *prowapi.ProwJob 1187 expected []string 1188 }{ 1189 { 1190 name: "Jobs that are not presubmit or batch are ignored", 1191 pj: getProwJob(prowapi.PeriodicJob, "org", "", "repo", "baseSHA", prowapi.SuccessState, []prowapi.Pull{{SHA: "head"}}), 1192 }, 1193 { 1194 name: "Non-Passing jobs are ignored", 1195 pj: getProwJob(prowapi.PresubmitJob, "org", "repo", "", "baseSHA", prowapi.FailureState, []prowapi.Pull{{SHA: "head"}}), 1196 }, 1197 { 1198 name: "Indexkey is returned for presubmit job", 1199 pj: getProwJob(prowapi.PresubmitJob, "org", "repo", "", "baseSHA", prowapi.SuccessState, []prowapi.Pull{{SHA: "head"}}), 1200 expected: []string{"org/repo@baseSHA+head"}, 1201 }, 1202 { 1203 name: "Indexkeys are returned for batch job", 1204 pj: getProwJob(prowapi.BatchJob, "org", "repo", "", "baseSHA", prowapi.SuccessState, []prowapi.Pull{{SHA: "head"}, {SHA: "head-2"}}), 1205 expected: []string{"org/repo@baseSHA+head", "org/repo@baseSHA+head-2"}, 1206 }, 1207 } 1208 for _, tc := range testCases { 1209 t.Run(tc.name, func(t *testing.T) { 1210 var results []string 1211 results = append(results, indexFuncPassingJobs(tc.pj)...) 1212 if diff := deep.Equal(tc.expected, results); diff != nil { 1213 t.Errorf("expected does not match result, diff: %v", diff) 1214 } 1215 }) 1216 } 1217 } 1218 1219 func TestSetStatusRespectsRequiredContexts(t *testing.T) { 1220 var pr PullRequest 1221 pr.Commits.Nodes = []struct{ Commit Commit }{{}} 1222 pr.Repository.NameWithOwner = githubql.String("org/repo") 1223 pr.Number = githubql.Int(2) 1224 requiredContexts := map[string][]string{"org/repo#2": {"foo", "bar"}} 1225 1226 fghc := &fgc{ 1227 refs: map[string]string{"/ heads/": "SHA"}, 1228 } 1229 log := logrus.WithField("component", "tide") 1230 initialLog, err := log.String() 1231 if err != nil { 1232 t.Fatalf("Failed to get log output before testing: %v", err) 1233 } 1234 1235 ca := &config.Agent{} 1236 ca.Set(&config.Config{}) 1237 1238 sc := &statusController{ 1239 logger: log, 1240 ghc: fghc, 1241 config: ca.Config, 1242 pjClient: fakectrlruntimeclient.NewClientBuilder().Build(), 1243 ghProvider: &GitHubProvider{ 1244 ghc: fghc, 1245 mergeChecker: newMergeChecker(ca.Config, fghc), 1246 }, 1247 statusUpdate: &statusUpdate{ 1248 dontUpdateStatus: &threadSafePRSet{}, 1249 newPoolPending: make(chan bool), 1250 }, 1251 } 1252 crc := CodeReviewCommonFromPullRequest(&pr) 1253 pool := map[string]CodeReviewCommon{prKey(crc): *crc} 1254 sc.setStatuses([]CodeReviewCommon{*crc}, pool, blockers.Blockers{}, nil, requiredContexts) 1255 if str, err := log.String(); err != nil { 1256 t.Fatalf("Failed to get log output: %v", err) 1257 } else if str != initialLog { 1258 t.Errorf("Error setting status: %s", str) 1259 } 1260 1261 if n := len(fghc.statuses); n != 1 { 1262 t.Fatalf("expected exactly one status to be set, got %d", n) 1263 } 1264 1265 expectedDescription := "Not mergeable. Retesting: bar foo" 1266 val, exists := fghc.statuses["//"] 1267 if !exists { 1268 t.Fatal("Status didn't get set") 1269 } 1270 if val.Description != expectedDescription { 1271 t.Errorf("Expected description to be %q, was %q", expectedDescription, val.Description) 1272 } 1273 } 1274 1275 func TestNewBaseSHAGetter(t *testing.T) { 1276 org, repo, branch := "org", "repo", "branch" 1277 testCases := []struct { 1278 name string 1279 baseSHAs map[string]string 1280 ghc githubClient 1281 1282 expectedSHA string 1283 expectErr bool 1284 }{ 1285 { 1286 name: "Default to content of baseSHAs map", 1287 baseSHAs: map[string]string{"org/repo:branch": "123"}, 1288 expectedSHA: "123", 1289 }, 1290 { 1291 name: "BaseSHAs map has no entry, ask GitHub", 1292 baseSHAs: map[string]string{}, 1293 ghc: &fgc{refs: map[string]string{"org/repo heads/branch": "SHA"}}, 1294 expectedSHA: "SHA", 1295 }, 1296 { 1297 name: "Error is returned", 1298 baseSHAs: map[string]string{}, 1299 ghc: &fgc{err: errors.New("some-failure")}, 1300 expectErr: true, 1301 }, 1302 } 1303 1304 for _, tc := range testCases { 1305 t.Run(tc.name, func(t *testing.T) { 1306 result, err := newBaseSHAGetter(tc.baseSHAs, tc.ghc, org, repo, branch)() 1307 if err != nil && !tc.expectErr { 1308 t.Fatalf("unexpected error: %v", err) 1309 } 1310 if tc.expectErr { 1311 return 1312 } 1313 if result != tc.expectedSHA { 1314 t.Errorf("expected %q, got %q", tc.expectedSHA, result) 1315 } 1316 if val := tc.baseSHAs[org+"/"+repo+":"+branch]; val != tc.expectedSHA { 1317 t.Errorf("baseSHA in the map (%q) does not match expected(%q)", val, tc.expectedSHA) 1318 } 1319 }) 1320 } 1321 } 1322 1323 func TestStatusControllerSearch(t *testing.T) { 1324 t.Parallel() 1325 testCases := []struct { 1326 name string 1327 prs map[string][]PullRequest 1328 usesAppsAuth bool 1329 1330 expected []CodeReviewCommon 1331 }{ 1332 { 1333 name: "Apps auth: Query gets split by org", 1334 prs: map[string][]PullRequest{ 1335 "org-a": {{Number: githubql.Int(1)}}, 1336 "org-b": {{Number: githubql.Int(2)}}, 1337 }, 1338 usesAppsAuth: true, 1339 expected: []CodeReviewCommon{ 1340 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 1}), 1341 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 2}), 1342 }, 1343 }, 1344 { 1345 name: "No apps auth: Query remains unsplit", 1346 prs: map[string][]PullRequest{ 1347 "": {{Number: githubql.Int(1)}, {Number: githubql.Int(2)}}, 1348 }, 1349 usesAppsAuth: false, 1350 expected: []CodeReviewCommon{ 1351 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 1}), 1352 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 2}), 1353 }, 1354 }, 1355 } 1356 1357 for _, tc := range testCases { 1358 t.Run(tc.name, func(t *testing.T) { 1359 ghc := &fgc{prs: tc.prs} 1360 cfg := func() *config.Config { 1361 return &config.Config{ProwConfig: config.ProwConfig{Tide: config.Tide{ 1362 TideGitHubConfig: config.TideGitHubConfig{Queries: config.TideQueries{{Orgs: []string{"org-a", "org-b"}}}}}}} 1363 } 1364 sc, err := newStatusController( 1365 context.Background(), 1366 logrus.WithField("tc", tc), 1367 ghc, 1368 newFakeManager(), 1369 nil, 1370 cfg, 1371 nil, 1372 "", 1373 nil, 1374 tc.usesAppsAuth, 1375 &statusUpdate{ 1376 dontUpdateStatus: &threadSafePRSet{}, 1377 newPoolPending: make(chan bool), 1378 }, 1379 ) 1380 if err != nil { 1381 t.Fatalf("failed to construct status controller: %v", err) 1382 } 1383 1384 result := sc.search() 1385 if diff := cmp.Diff(result, tc.expected, cmpopts.SortSlices(func(a, b CodeReviewCommon) bool { return a.Number < b.Number })); diff != "" { 1386 t.Errorf("result differs from expected: %s", diff) 1387 } 1388 }) 1389 } 1390 }