sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/tide/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 tide 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "math/rand" 25 "net/http" 26 "net/http/httptest" 27 "reflect" 28 "sort" 29 "strconv" 30 "strings" 31 "sync" 32 "testing" 33 "text/template" 34 "time" 35 36 "github.com/go-test/deep" 37 "github.com/google/go-cmp/cmp" 38 "github.com/google/go-cmp/cmp/cmpopts" 39 fuzz "github.com/google/gofuzz" 40 githubql "github.com/shurcooL/githubv4" 41 "github.com/sirupsen/logrus" 42 "github.com/sirupsen/logrus/hooks/test" 43 "github.com/stretchr/testify/assert" 44 apiequality "k8s.io/apimachinery/pkg/api/equality" 45 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 46 "k8s.io/apimachinery/pkg/runtime" 47 "k8s.io/apimachinery/pkg/util/diff" 48 "k8s.io/apimachinery/pkg/util/sets" 49 utilpointer "k8s.io/utils/pointer" 50 ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" 51 fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" 52 53 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 54 "sigs.k8s.io/prow/pkg/config" 55 "sigs.k8s.io/prow/pkg/git/localgit" 56 "sigs.k8s.io/prow/pkg/git/types" 57 "sigs.k8s.io/prow/pkg/git/v2" 58 "sigs.k8s.io/prow/pkg/github" 59 "sigs.k8s.io/prow/pkg/kube" 60 "sigs.k8s.io/prow/pkg/tide/history" 61 ) 62 63 func init() { 64 // Debugging tests without this isn't fun 65 logrus.SetLevel(logrus.DebugLevel) 66 } 67 68 var defaultBranch = localgit.DefaultBranch("") 69 70 func testPullsMatchList(t *testing.T, test string, actual []CodeReviewCommon, expected []int) { 71 if len(actual) != len(expected) { 72 t.Errorf("Wrong size for case %s. Got PRs %+v, wanted numbers %v.", test, actual, expected) 73 return 74 } 75 for _, pr := range actual { 76 var found bool 77 n1 := int(pr.Number) 78 for _, n2 := range expected { 79 if n1 == n2 { 80 found = true 81 } 82 } 83 if !found { 84 t.Errorf("For case %s, found PR %d but shouldn't have.", test, n1) 85 } 86 } 87 } 88 89 func TestAccumulateBatch(t *testing.T) { 90 jobSet := []config.Presubmit{ 91 { 92 Reporter: config.Reporter{Context: "foo"}, 93 }, 94 { 95 Reporter: config.Reporter{Context: "bar"}, 96 }, 97 { 98 Reporter: config.Reporter{Context: "baz"}, 99 }, 100 } 101 type pull struct { 102 number int 103 sha string 104 } 105 type prowjob struct { 106 prs []pull 107 job string 108 state prowapi.ProwJobState 109 } 110 tests := []struct { 111 name string 112 presubmits []config.Presubmit 113 pulls []pull 114 prowJobs []prowjob 115 prowYAMLGetter config.ProwYAMLGetter 116 117 merges []int 118 pending bool 119 }{ 120 { 121 name: "no batches running", 122 }, 123 { 124 name: "batch pending", 125 presubmits: []config.Presubmit{ 126 {Reporter: config.Reporter{Context: "foo"}}, 127 }, 128 pulls: []pull{{1, "a"}, {2, "b"}}, 129 prowJobs: []prowjob{{job: "foo", state: prowapi.PendingState, prs: []pull{{1, "a"}}}}, 130 pending: true, 131 }, 132 { 133 name: "pending batch missing presubmits is ignored", 134 presubmits: jobSet, 135 pulls: []pull{{1, "a"}, {2, "b"}}, 136 prowJobs: []prowjob{{job: "foo", state: prowapi.PendingState, prs: []pull{{1, "a"}}}}, 137 }, 138 { 139 name: "batch pending, successful previous run", 140 presubmits: jobSet, 141 pulls: []pull{{1, "a"}, {2, "b"}}, 142 prowJobs: []prowjob{ 143 {job: "foo", state: prowapi.PendingState, prs: []pull{{1, "a"}}}, 144 {job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}}}, 145 {job: "baz", state: prowapi.SuccessState, prs: []pull{{1, "a"}}}, 146 {job: "foo", state: prowapi.SuccessState, prs: []pull{{2, "b"}}}, 147 {job: "bar", state: prowapi.SuccessState, prs: []pull{{2, "b"}}}, 148 {job: "baz", state: prowapi.SuccessState, prs: []pull{{2, "b"}}}, 149 }, 150 pending: true, 151 merges: []int{2}, 152 }, 153 { 154 name: "successful run", 155 presubmits: jobSet, 156 pulls: []pull{{1, "a"}, {2, "b"}}, 157 prowJobs: []prowjob{ 158 {job: "foo", state: prowapi.SuccessState, prs: []pull{{2, "b"}}}, 159 {job: "bar", state: prowapi.SuccessState, prs: []pull{{2, "b"}}}, 160 {job: "baz", state: prowapi.SuccessState, prs: []pull{{2, "b"}}}, 161 }, 162 merges: []int{2}, 163 }, 164 { 165 name: "successful run, multiple PRs", 166 presubmits: jobSet, 167 pulls: []pull{{1, "a"}, {2, "b"}}, 168 prowJobs: []prowjob{ 169 {job: "foo", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 170 {job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 171 {job: "baz", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 172 }, 173 merges: []int{1, 2}, 174 }, 175 { 176 name: "successful run, failures in past", 177 presubmits: jobSet, 178 pulls: []pull{{1, "a"}, {2, "b"}}, 179 prowJobs: []prowjob{ 180 {job: "foo", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 181 {job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 182 {job: "baz", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 183 {job: "foo", state: prowapi.FailureState, prs: []pull{{1, "a"}, {2, "b"}}}, 184 {job: "baz", state: prowapi.FailureState, prs: []pull{{1, "a"}, {2, "b"}}}, 185 {job: "foo", state: prowapi.FailureState, prs: []pull{{1, "c"}, {2, "b"}}}, 186 }, 187 merges: []int{1, 2}, 188 }, 189 { 190 name: "failures", 191 presubmits: jobSet, 192 pulls: []pull{{1, "a"}, {2, "b"}}, 193 prowJobs: []prowjob{ 194 {job: "foo", state: prowapi.FailureState, prs: []pull{{1, "a"}, {2, "b"}}}, 195 {job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 196 {job: "baz", state: prowapi.FailureState, prs: []pull{{1, "a"}, {2, "b"}}}, 197 {job: "foo", state: prowapi.FailureState, prs: []pull{{1, "c"}, {2, "b"}}}, 198 }, 199 }, 200 { 201 name: "missing job required by one PR", 202 presubmits: jobSet, 203 pulls: []pull{{1, "a"}, {2, "b"}}, 204 prowJobs: []prowjob{ 205 {job: "foo", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 206 {job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 207 {job: "baz", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 208 }, 209 prowYAMLGetter: prowYAMLGetterForHeadRefs([]string{"a", "b"}, []config.Presubmit{{ 210 AlwaysRun: true, 211 Reporter: config.Reporter{Context: "boo"}, 212 }}), 213 }, 214 { 215 name: "successful run with PR that requires additional job", 216 presubmits: jobSet, 217 pulls: []pull{{1, "a"}, {2, "b"}}, 218 prowJobs: []prowjob{ 219 {job: "foo", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 220 {job: "bar", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 221 {job: "baz", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 222 {job: "boo", state: prowapi.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 223 }, 224 merges: []int{1, 2}, 225 }, 226 { 227 name: "no presubmits", 228 pulls: []pull{{1, "a"}, {2, "b"}}, 229 pending: false, 230 }, 231 { 232 name: "pending batch with PR that left pool, successful previous run", 233 presubmits: jobSet, 234 pulls: []pull{{2, "b"}}, 235 prowJobs: []prowjob{ 236 {job: "foo", state: prowapi.PendingState, prs: []pull{{1, "a"}}}, 237 {job: "foo", state: prowapi.SuccessState, prs: []pull{{2, "b"}}}, 238 {job: "bar", state: prowapi.SuccessState, prs: []pull{{2, "b"}}}, 239 {job: "baz", state: prowapi.SuccessState, prs: []pull{{2, "b"}}}, 240 }, 241 pending: false, 242 merges: []int{2}, 243 }, 244 } 245 for _, test := range tests { 246 t.Run(test.name, func(t *testing.T) { 247 248 var pulls []CodeReviewCommon 249 for _, p := range test.pulls { 250 pr := PullRequest{ 251 Number: githubql.Int(p.number), 252 HeadRefOID: githubql.String(p.sha), 253 } 254 pulls = append(pulls, *CodeReviewCommonFromPullRequest(&pr)) 255 } 256 var pjs []prowapi.ProwJob 257 for _, pj := range test.prowJobs { 258 npj := prowapi.ProwJob{ 259 Spec: prowapi.ProwJobSpec{ 260 Job: pj.job, 261 Context: pj.job, 262 Type: prowapi.BatchJob, 263 Refs: new(prowapi.Refs), 264 }, 265 Status: prowapi.ProwJobStatus{State: pj.state}, 266 } 267 for _, pr := range pj.prs { 268 npj.Spec.Refs.Pulls = append(npj.Spec.Refs.Pulls, prowapi.Pull{ 269 Number: pr.number, 270 SHA: pr.sha, 271 }) 272 } 273 pjs = append(pjs, npj) 274 } 275 for idx := range test.presubmits { 276 test.presubmits[idx].AlwaysRun = true 277 } 278 279 inrepoconfig := config.InRepoConfig{} 280 if test.prowYAMLGetter != nil { 281 inrepoconfig.Enabled = map[string]*bool{"*": utilpointer.Bool(true)} 282 } 283 cfg := func() *config.Config { 284 return &config.Config{ 285 JobConfig: config.JobConfig{ 286 PresubmitsStatic: map[string][]config.Presubmit{ 287 "org/repo": test.presubmits, 288 }, 289 ProwYAMLGetterWithDefaults: test.prowYAMLGetter, 290 }, 291 ProwConfig: config.ProwConfig{ 292 InRepoConfig: inrepoconfig, 293 }, 294 } 295 } 296 c := &syncController{ 297 config: cfg, 298 provider: newGitHubProvider(logrus.WithContext(context.Background()), nil, nil, cfg, nil, false), 299 changedFiles: &changedFilesAgent{}, 300 logger: logrus.WithField("test", test.name), 301 } 302 merges, pending := c.accumulateBatch(subpool{org: "org", repo: "repo", prs: pulls, pjs: pjs, log: logrus.WithField("test", test.name)}) 303 if (len(pending) > 0) != test.pending { 304 t.Errorf("For case \"%s\", got wrong pending.", test.name) 305 } 306 testPullsMatchList(t, test.name, merges, test.merges) 307 }) 308 } 309 } 310 311 func TestAccumulate(t *testing.T) { 312 313 const baseSHA = "8d287a3aeae90fd0aef4a70009c715712ff302cd" 314 jobSet := []config.Presubmit{ 315 { 316 Reporter: config.Reporter{ 317 Context: "job1", 318 }, 319 }, 320 { 321 Reporter: config.Reporter{ 322 Context: "job2", 323 }, 324 }, 325 } 326 type prowjob struct { 327 prNumber int 328 job string 329 state prowapi.ProwJobState 330 sha string 331 } 332 tests := []struct { 333 name string 334 presubmits map[int][]config.Presubmit 335 pullRequests map[int]string 336 pullRequestModifier func(*PullRequest) 337 prowJobs []prowjob 338 339 successes []int 340 pendings []int 341 none []int 342 }{ 343 { 344 pullRequests: map[int]string{1: "", 2: "", 3: "", 4: "", 5: "", 6: "", 7: ""}, 345 presubmits: map[int][]config.Presubmit{ 346 1: jobSet, 347 2: jobSet, 348 3: jobSet, 349 4: jobSet, 350 5: jobSet, 351 6: jobSet, 352 7: jobSet, 353 }, 354 prowJobs: []prowjob{ 355 {2, "job1", prowapi.PendingState, ""}, 356 {3, "job1", prowapi.PendingState, ""}, 357 {3, "job2", prowapi.TriggeredState, ""}, 358 {4, "job1", prowapi.FailureState, ""}, 359 {4, "job2", prowapi.PendingState, ""}, 360 {5, "job1", prowapi.PendingState, ""}, 361 {5, "job2", prowapi.FailureState, ""}, 362 {5, "job2", prowapi.PendingState, ""}, 363 {6, "job1", prowapi.SuccessState, ""}, 364 {6, "job2", prowapi.PendingState, ""}, 365 {7, "job1", prowapi.SuccessState, ""}, 366 {7, "job2", prowapi.SuccessState, ""}, 367 {7, "job1", prowapi.FailureState, ""}, 368 }, 369 370 successes: []int{7}, 371 pendings: []int{3, 5, 6}, 372 none: []int{1, 2, 4}, 373 }, 374 { 375 pullRequests: map[int]string{7: ""}, 376 presubmits: map[int][]config.Presubmit{ 377 7: { 378 {Reporter: config.Reporter{Context: "job1"}}, 379 {Reporter: config.Reporter{Context: "job2"}}, 380 {Reporter: config.Reporter{Context: "job3"}}, 381 {Reporter: config.Reporter{Context: "job4"}}, 382 }, 383 }, 384 prowJobs: []prowjob{ 385 {7, "job1", prowapi.SuccessState, ""}, 386 {7, "job2", prowapi.FailureState, ""}, 387 {7, "job3", prowapi.FailureState, ""}, 388 {7, "job4", prowapi.FailureState, ""}, 389 {7, "job3", prowapi.FailureState, ""}, 390 {7, "job4", prowapi.FailureState, ""}, 391 {7, "job2", prowapi.SuccessState, ""}, 392 {7, "job3", prowapi.SuccessState, ""}, 393 {7, "job4", prowapi.FailureState, ""}, 394 }, 395 396 successes: []int{}, 397 pendings: []int{}, 398 none: []int{7}, 399 }, 400 { 401 pullRequests: map[int]string{7: ""}, 402 presubmits: map[int][]config.Presubmit{ 403 7: { 404 {Reporter: config.Reporter{Context: "job1"}}, 405 {Reporter: config.Reporter{Context: "job2"}}, 406 {Reporter: config.Reporter{Context: "job3"}}, 407 {Reporter: config.Reporter{Context: "job4"}}, 408 }, 409 }, 410 prowJobs: []prowjob{ 411 {7, "job1", prowapi.FailureState, ""}, 412 {7, "job2", prowapi.FailureState, ""}, 413 {7, "job3", prowapi.FailureState, ""}, 414 {7, "job4", prowapi.FailureState, ""}, 415 {7, "job3", prowapi.FailureState, ""}, 416 {7, "job4", prowapi.FailureState, ""}, 417 {7, "job2", prowapi.FailureState, ""}, 418 {7, "job3", prowapi.FailureState, ""}, 419 {7, "job4", prowapi.FailureState, ""}, 420 }, 421 422 successes: []int{}, 423 pendings: []int{}, 424 none: []int{7}, 425 }, 426 { 427 pullRequests: map[int]string{7: ""}, 428 presubmits: map[int][]config.Presubmit{ 429 7: { 430 {Reporter: config.Reporter{Context: "job1"}}, 431 {Reporter: config.Reporter{Context: "job2"}}, 432 {Reporter: config.Reporter{Context: "job3"}}, 433 {Reporter: config.Reporter{Context: "job4"}}, 434 }, 435 }, 436 prowJobs: []prowjob{ 437 {7, "job1", prowapi.SuccessState, ""}, 438 {7, "job2", prowapi.FailureState, ""}, 439 {7, "job3", prowapi.FailureState, ""}, 440 {7, "job4", prowapi.FailureState, ""}, 441 {7, "job3", prowapi.FailureState, ""}, 442 {7, "job4", prowapi.FailureState, ""}, 443 {7, "job2", prowapi.SuccessState, ""}, 444 {7, "job3", prowapi.SuccessState, ""}, 445 {7, "job4", prowapi.SuccessState, ""}, 446 {7, "job1", prowapi.FailureState, ""}, 447 }, 448 449 successes: []int{7}, 450 pendings: []int{}, 451 none: []int{}, 452 }, 453 { 454 pullRequests: map[int]string{7: ""}, 455 presubmits: map[int][]config.Presubmit{ 456 7: { 457 {Reporter: config.Reporter{Context: "job1"}}, 458 {Reporter: config.Reporter{Context: "job2"}}, 459 {Reporter: config.Reporter{Context: "job3"}}, 460 {Reporter: config.Reporter{Context: "job4"}}, 461 }, 462 }, 463 prowJobs: []prowjob{ 464 {7, "job1", prowapi.SuccessState, ""}, 465 {7, "job2", prowapi.FailureState, ""}, 466 {7, "job3", prowapi.FailureState, ""}, 467 {7, "job4", prowapi.FailureState, ""}, 468 {7, "job3", prowapi.FailureState, ""}, 469 {7, "job4", prowapi.FailureState, ""}, 470 {7, "job2", prowapi.SuccessState, ""}, 471 {7, "job3", prowapi.SuccessState, ""}, 472 {7, "job4", prowapi.PendingState, ""}, 473 {7, "job1", prowapi.FailureState, ""}, 474 }, 475 476 successes: []int{}, 477 pendings: []int{7}, 478 none: []int{}, 479 }, 480 { 481 presubmits: map[int][]config.Presubmit{ 482 7: { 483 {Reporter: config.Reporter{Context: "job1"}}, 484 }, 485 }, 486 pullRequests: map[int]string{7: "new", 8: "new"}, 487 prowJobs: []prowjob{ 488 {7, "job1", prowapi.SuccessState, "old"}, 489 {7, "job1", prowapi.FailureState, "new"}, 490 {8, "job1", prowapi.FailureState, "old"}, 491 {8, "job1", prowapi.SuccessState, "new"}, 492 }, 493 494 successes: []int{8}, 495 pendings: []int{}, 496 none: []int{7}, 497 }, 498 { 499 pullRequests: map[int]string{7: "new", 8: "new"}, 500 prowJobs: []prowjob{}, 501 502 successes: []int{8, 7}, 503 pendings: []int{}, 504 none: []int{}, 505 }, 506 { 507 name: "Results from successful status context for which we do not have a prowjob anymore are considered", 508 presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}}, 509 pullRequests: map[int]string{1: "headsha"}, 510 pullRequestModifier: func(pr *PullRequest) { 511 pr.Commits.Nodes = []struct{ Commit Commit }{{ 512 Commit: Commit{ 513 OID: githubql.String("headsha"), 514 Status: CommitStatus{Contexts: []Context{{ 515 Context: githubql.String("job1"), 516 Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA), 517 State: githubql.StatusStateSuccess, 518 }}}}, 519 }} 520 }, 521 522 successes: []int{1}, 523 }, 524 { 525 name: "Results from successful status context for wrong baseSHA is ignored", 526 presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}}, 527 pullRequests: map[int]string{1: "headsha"}, 528 pullRequestModifier: func(pr *PullRequest) { 529 pr.Commits.Nodes = []struct{ Commit Commit }{{ 530 Commit: Commit{ 531 OID: githubql.String("headsha"), 532 Status: CommitStatus{Contexts: []Context{{ 533 Context: githubql.String("job1"), 534 Description: githubql.String("Job succeeded. BaseSHA:c22a32add1a36daf3b16af3762b3922e70c9626a"), 535 State: githubql.StatusStateSuccess, 536 }}}}, 537 }} 538 }, 539 540 none: []int{1}, 541 }, 542 { 543 name: "Results from failed status context for which we do not have a prowjob anymore are irrelevant", 544 presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}}, 545 pullRequests: map[int]string{1: "headsha"}, 546 pullRequestModifier: func(pr *PullRequest) { 547 pr.Commits.Nodes = []struct{ Commit Commit }{{ 548 Commit: Commit{ 549 OID: githubql.String("headsha"), 550 Status: CommitStatus{Contexts: []Context{{ 551 Context: githubql.String("job1"), 552 Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA), 553 State: githubql.StatusStateFailure, 554 }}}}, 555 }} 556 }, 557 558 none: []int{1}, 559 }, 560 { 561 name: "Successful status context and prowjob, success", 562 presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}}, 563 pullRequests: map[int]string{1: "headsha"}, 564 pullRequestModifier: func(pr *PullRequest) { 565 pr.Commits.Nodes = []struct{ Commit Commit }{{ 566 Commit: Commit{ 567 OID: githubql.String("headsha"), 568 Status: CommitStatus{Contexts: []Context{{ 569 Context: githubql.String("job1"), 570 Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA), 571 State: githubql.StatusStateSuccess, 572 }}}}, 573 }} 574 }, 575 prowJobs: []prowjob{{1, "job1", prowapi.SuccessState, "headsha"}}, 576 577 successes: []int{1}, 578 }, 579 { 580 name: "Successful status context, failed prowjob, success", 581 presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}}, 582 pullRequests: map[int]string{1: "headsha"}, 583 pullRequestModifier: func(pr *PullRequest) { 584 pr.Commits.Nodes = []struct{ Commit Commit }{{ 585 Commit: Commit{ 586 OID: githubql.String("headsha"), 587 Status: CommitStatus{Contexts: []Context{{ 588 Context: githubql.String("job1"), 589 Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA), 590 State: githubql.StatusStateSuccess, 591 }}}}, 592 }} 593 }, 594 prowJobs: []prowjob{{1, "job1", prowapi.FailureState, "headsha"}}, 595 596 successes: []int{1}, 597 }, 598 { 599 name: "Failed status context, successful prowjob, success", 600 presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}}, 601 pullRequests: map[int]string{1: "headsha"}, 602 pullRequestModifier: func(pr *PullRequest) { 603 pr.Commits.Nodes = []struct{ Commit Commit }{{ 604 Commit: Commit{ 605 OID: githubql.String("headsha"), 606 Status: CommitStatus{Contexts: []Context{{ 607 Context: githubql.String("job1"), 608 Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA), 609 State: githubql.StatusStateFailure, 610 }}}}, 611 }} 612 }, 613 prowJobs: []prowjob{{1, "job1", prowapi.SuccessState, "headsha"}}, 614 615 successes: []int{1}, 616 }, 617 { 618 name: "Failed status context and prowjob, failure", 619 presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job1"}}}}, 620 pullRequests: map[int]string{1: "headsha"}, 621 pullRequestModifier: func(pr *PullRequest) { 622 pr.Commits.Nodes = []struct{ Commit Commit }{{ 623 Commit: Commit{ 624 OID: githubql.String("headsha"), 625 Status: CommitStatus{Contexts: []Context{{ 626 Context: githubql.String("job1"), 627 Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA), 628 State: githubql.StatusStateFailure, 629 }}}}, 630 }} 631 }, 632 prowJobs: []prowjob{{1, "job1", prowapi.FailureState, "headsha"}}, 633 634 none: []int{1}, 635 }, 636 { 637 name: "Mixture of results from status context and prowjobs", 638 presubmits: map[int][]config.Presubmit{1: { 639 {Reporter: config.Reporter{Context: "job1"}}, 640 {Reporter: config.Reporter{Context: "job2"}}, 641 }}, 642 pullRequests: map[int]string{1: "headsha"}, 643 pullRequestModifier: func(pr *PullRequest) { 644 pr.Commits.Nodes = []struct{ Commit Commit }{{ 645 Commit: Commit{ 646 OID: githubql.String("headsha"), 647 Status: CommitStatus{Contexts: []Context{{ 648 Context: githubql.String("job1"), 649 Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA), 650 State: githubql.StatusStateSuccess, 651 }}}}, 652 }} 653 }, 654 prowJobs: []prowjob{{1, "job2", prowapi.SuccessState, "headsha"}}, 655 656 successes: []int{1}, 657 }, 658 } 659 660 for i, test := range tests { 661 if test.name == "" { 662 test.name = strconv.Itoa(i) 663 } 664 t.Run(test.name, func(t *testing.T) { 665 syncCtrl := &syncController{ 666 provider: &GitHubProvider{ghc: &fgc{}, logger: logrus.NewEntry(logrus.New())}, 667 logger: logrus.NewEntry(logrus.New()), 668 } 669 var pulls []CodeReviewCommon 670 for num, sha := range test.pullRequests { 671 newPull := PullRequest{Number: githubql.Int(num), HeadRefOID: githubql.String(sha)} 672 if test.pullRequestModifier != nil { 673 test.pullRequestModifier(&newPull) 674 } 675 pulls = append(pulls, *CodeReviewCommonFromPullRequest(&newPull)) 676 } 677 var pjs []prowapi.ProwJob 678 for _, pj := range test.prowJobs { 679 pjs = append(pjs, prowapi.ProwJob{ 680 Spec: prowapi.ProwJobSpec{ 681 Job: pj.job, 682 Context: pj.job, 683 Type: prowapi.PresubmitJob, 684 Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{Number: pj.prNumber, SHA: pj.sha}}}, 685 }, 686 Status: prowapi.ProwJobStatus{State: pj.state}, 687 }) 688 } 689 690 successes, pendings, nones, _ := syncCtrl.accumulate(test.presubmits, pulls, pjs, baseSHA) 691 692 t.Logf("test run %d", i) 693 testPullsMatchList(t, "successes", successes, test.successes) 694 testPullsMatchList(t, "pendings", pendings, test.pendings) 695 testPullsMatchList(t, "nones", nones, test.none) 696 }) 697 } 698 } 699 700 type fgc struct { 701 err error 702 lock sync.Mutex 703 704 prs map[string][]PullRequest 705 refs map[string]string 706 merged int 707 setStatus bool 708 statuses map[string]github.Status 709 mergeErrs map[int]error 710 queryCalls int 711 712 expectedSHA string 713 skipExpectedShaCheck bool 714 combinedStatus map[string]string 715 checkRuns *github.CheckRunList 716 } 717 718 func (f *fgc) GetRepo(o, r string) (github.FullRepo, error) { 719 repo := github.FullRepo{} 720 if strings.Contains(r, "squash") { 721 repo.AllowSquashMerge = true 722 } 723 if strings.Contains(r, "rebase") { 724 repo.AllowRebaseMerge = true 725 } 726 if !strings.Contains(r, "nomerge") { 727 repo.AllowMergeCommit = true 728 } 729 return repo, nil 730 } 731 732 func (f *fgc) GetRef(o, r, ref string) (string, error) { 733 return f.refs[o+"/"+r+" "+ref], f.err 734 } 735 736 func (f *fgc) QueryWithGitHubAppsSupport(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error { 737 sq, ok := q.(*searchQuery) 738 if !ok { 739 return errors.New("unexpected query type") 740 } 741 742 f.lock.Lock() 743 defer f.lock.Unlock() 744 f.queryCalls++ 745 746 for _, pr := range f.prs[org] { 747 sq.Search.Nodes = append( 748 sq.Search.Nodes, 749 struct { 750 PullRequest PullRequest `graphql:"... on PullRequest"` 751 }{PullRequest: pr}, 752 ) 753 } 754 return nil 755 } 756 757 func (f *fgc) Merge(org, repo string, number int, details github.MergeDetails) error { 758 if err, ok := f.mergeErrs[number]; ok { 759 return err 760 } 761 f.merged++ 762 return nil 763 } 764 765 func (f *fgc) CreateStatus(org, repo, ref string, s github.Status) error { 766 f.lock.Lock() 767 defer f.lock.Unlock() 768 switch s.State { 769 case github.StatusSuccess, github.StatusError, github.StatusPending, github.StatusFailure: 770 if f.statuses == nil { 771 f.statuses = map[string]github.Status{} 772 } 773 f.statuses[org+"/"+repo+"/"+ref] = s 774 f.setStatus = true 775 return nil 776 } 777 return fmt.Errorf("invalid 'state' value: %q", s.State) 778 } 779 780 func (f *fgc) GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error) { 781 if !f.skipExpectedShaCheck && f.expectedSHA != ref { 782 return nil, errors.New("bad combined status request: incorrect sha") 783 } 784 var statuses []github.Status 785 for c, s := range f.combinedStatus { 786 statuses = append(statuses, github.Status{Context: c, State: s}) 787 } 788 return &github.CombinedStatus{ 789 Statuses: statuses, 790 }, 791 nil 792 } 793 794 func (f *fgc) ListCheckRuns(org, repo, ref string) (*github.CheckRunList, error) { 795 if !f.skipExpectedShaCheck && f.expectedSHA != ref { 796 return nil, errors.New("bad combined status request: incorrect sha") 797 } 798 if f.checkRuns != nil { 799 return f.checkRuns, nil 800 } 801 return &github.CheckRunList{}, nil 802 } 803 804 func (f *fgc) GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) { 805 if number != 100 { 806 return nil, nil 807 } 808 return []github.PullRequestChange{ 809 { 810 Filename: "CHANGED", 811 }, 812 }, 813 nil 814 } 815 816 // TestDividePool ensures that subpools returned by dividePool satisfy a few 817 // important invariants. 818 func TestDividePool(t *testing.T) { 819 testPulls := []struct { 820 org string 821 repo string 822 number int 823 branch string 824 }{ 825 { 826 org: "k", 827 repo: "t-i", 828 number: 5, 829 branch: defaultBranch, 830 }, 831 { 832 org: "k", 833 repo: "t-i", 834 number: 6, 835 branch: defaultBranch, 836 }, 837 { 838 org: "k", 839 repo: "k", 840 number: 123, 841 branch: defaultBranch, 842 }, 843 { 844 org: "k", 845 repo: "k", 846 number: 1000, 847 branch: "release-1.6", 848 }, 849 } 850 testPJs := []struct { 851 jobType prowapi.ProwJobType 852 org string 853 repo string 854 baseRef string 855 baseSHA string 856 }{ 857 { 858 jobType: prowapi.PresubmitJob, 859 org: "k", 860 repo: "t-i", 861 baseRef: defaultBranch, 862 baseSHA: "123", 863 }, 864 { 865 jobType: prowapi.BatchJob, 866 org: "k", 867 repo: "t-i", 868 baseRef: defaultBranch, 869 baseSHA: "123", 870 }, 871 { 872 jobType: prowapi.PeriodicJob, 873 }, 874 { 875 jobType: prowapi.PresubmitJob, 876 org: "k", 877 repo: "t-i", 878 baseRef: "patch", 879 baseSHA: "123", 880 }, 881 { 882 jobType: prowapi.PresubmitJob, 883 org: "k", 884 repo: "t-i", 885 baseRef: defaultBranch, 886 baseSHA: "abc", 887 }, 888 { 889 jobType: prowapi.PresubmitJob, 890 org: "o", 891 repo: "t-i", 892 baseRef: defaultBranch, 893 baseSHA: "123", 894 }, 895 { 896 jobType: prowapi.PresubmitJob, 897 org: "k", 898 repo: "other", 899 baseRef: defaultBranch, 900 baseSHA: "123", 901 }, 902 } 903 fc := &fgc{ 904 refs: map[string]string{ 905 "k/t-i heads/master": "123", 906 "k/k heads/master": "456", 907 "k/k heads/release-1.6": "789", 908 }, 909 } 910 911 configGetter := func() *config.Config { 912 return &config.Config{ 913 ProwConfig: config.ProwConfig{ 914 ProwJobNamespace: "default", 915 }, 916 } 917 } 918 919 mmc := newMergeChecker(configGetter, fc) 920 log := logrus.NewEntry(logrus.StandardLogger()) 921 ghProvider := newGitHubProvider(log, fc, nil, configGetter, mmc, false) 922 mgr := newFakeManager() 923 c, err := newSyncController( 924 context.Background(), 925 log, 926 mgr, 927 ghProvider, 928 configGetter, 929 nil, 930 nil, 931 false, 932 &statusUpdate{ 933 dontUpdateStatus: &threadSafePRSet{}, 934 newPoolPending: make(chan bool), 935 }, 936 ) 937 if err != nil { 938 t.Fatalf("failed to construct sync controller: %v", err) 939 } 940 for idx, pj := range testPJs { 941 prowjob := &prowapi.ProwJob{ 942 ObjectMeta: metav1.ObjectMeta{ 943 Name: fmt.Sprintf("pj-%d", idx), 944 Namespace: "default", 945 }, 946 Spec: prowapi.ProwJobSpec{ 947 Type: pj.jobType, 948 Refs: &prowapi.Refs{ 949 Org: pj.org, 950 Repo: pj.repo, 951 BaseRef: pj.baseRef, 952 BaseSHA: pj.baseSHA, 953 }, 954 }, 955 } 956 if err := mgr.GetClient().Create(context.Background(), prowjob); err != nil { 957 t.Fatalf("failed to create prowjob: %v", err) 958 } 959 } 960 pulls := make(map[string]CodeReviewCommon) 961 for _, p := range testPulls { 962 npr := PullRequest{Number: githubql.Int(p.number)} 963 npr.BaseRef.Name = githubql.String(p.branch) 964 npr.BaseRef.Prefix = "refs/heads/" 965 npr.Repository.Name = githubql.String(p.repo) 966 npr.Repository.Owner.Login = githubql.String(p.org) 967 crc := CodeReviewCommonFromPullRequest(&npr) 968 pulls[prKey(crc)] = *crc 969 } 970 sps, err := c.dividePool(pulls) 971 if err != nil { 972 t.Fatalf("Error dividing pool: %v", err) 973 } 974 if len(sps) == 0 { 975 t.Error("No subpools.") 976 } 977 for _, sp := range sps { 978 name := fmt.Sprintf("%s/%s %s", sp.org, sp.repo, sp.branch) 979 sha := fc.refs[sp.org+"/"+sp.repo+" heads/"+sp.branch] 980 if sp.sha != sha { 981 t.Errorf("For subpool %s, got sha %q, expected %q.", name, sp.sha, sha) 982 } 983 if len(sp.prs) == 0 { 984 t.Errorf("Subpool %s has no PRs.", name) 985 } 986 for _, pr := range sp.prs { 987 if pr.Org != sp.org || pr.Repo != sp.repo || pr.BaseRefName != sp.branch { 988 t.Errorf("PR in wrong subpool. Got PR %+v in subpool %s.", pr, name) 989 } 990 } 991 for _, pj := range sp.pjs { 992 if pj.Spec.Type != prowapi.PresubmitJob && pj.Spec.Type != prowapi.BatchJob { 993 t.Errorf("PJ with bad type in subpool %s: %+v", name, pj) 994 } 995 referenceRef := &prowapi.Refs{ 996 Org: sp.org, 997 Repo: sp.repo, 998 BaseRef: sp.branch, 999 BaseSHA: sp.sha, 1000 } 1001 if diff := deep.Equal(pj.Spec.Refs, referenceRef); diff != nil { 1002 t.Errorf("Got PJ with wrong refs, diff: %v", diff) 1003 } 1004 } 1005 } 1006 } 1007 1008 func TestPickBatchV2(t *testing.T) { 1009 testPickBatch(localgit.NewV2, t) 1010 } 1011 1012 func testPickBatch(clients localgit.Clients, t *testing.T) { 1013 lg, gc, err := clients() 1014 if err != nil { 1015 t.Fatalf("Error making local git: %v", err) 1016 } 1017 defer gc.Clean() 1018 defer lg.Clean() 1019 if err := lg.MakeFakeRepo("o", "r"); err != nil { 1020 t.Fatalf("Error making fake repo: %v", err) 1021 } 1022 if err := lg.AddCommit("o", "r", map[string][]byte{"foo": []byte("foo")}); err != nil { 1023 t.Fatalf("Adding initial commit: %v", err) 1024 } 1025 testprs := []struct { 1026 files map[string][]byte 1027 success bool 1028 number int 1029 1030 included bool 1031 }{ 1032 { 1033 files: map[string][]byte{"bar": []byte("ok")}, 1034 success: true, 1035 number: 0, 1036 included: true, 1037 }, 1038 { 1039 files: map[string][]byte{"foo": []byte("ok")}, 1040 success: true, 1041 number: 1, 1042 included: true, 1043 }, 1044 { 1045 files: map[string][]byte{"bar": []byte("conflicts with 0")}, 1046 success: true, 1047 number: 2, 1048 included: false, 1049 }, 1050 { 1051 files: map[string][]byte{"something": []byte("ok")}, 1052 success: true, 1053 number: 3, 1054 included: true, 1055 }, 1056 { 1057 files: map[string][]byte{"changes": []byte("ok")}, 1058 success: true, 1059 number: 4, 1060 included: true, 1061 }, 1062 { 1063 files: map[string][]byte{"other": []byte("ok")}, 1064 success: true, 1065 number: 5, 1066 included: false, // excluded by context policy 1067 }, 1068 { 1069 files: map[string][]byte{"qux": []byte("ok")}, 1070 success: false, 1071 number: 6, 1072 included: false, 1073 }, 1074 { 1075 files: map[string][]byte{"bazel": []byte("ok")}, 1076 success: true, 1077 number: 7, 1078 included: true, 1079 }, 1080 { 1081 files: map[string][]byte{"bazel": []byte("ok")}, 1082 success: true, 1083 number: 8, 1084 included: false, // batch of 5 smallest excludes this 1085 }, 1086 } 1087 sp := subpool{ 1088 log: logrus.WithField("component", "tide"), 1089 org: "o", 1090 repo: "r", 1091 branch: defaultBranch, 1092 sha: defaultBranch, 1093 } 1094 for _, testpr := range testprs { 1095 if err := lg.CheckoutNewBranch("o", "r", fmt.Sprintf("pr-%d", testpr.number)); err != nil { 1096 t.Fatalf("Error checking out new branch: %v", err) 1097 } 1098 if err := lg.AddCommit("o", "r", testpr.files); err != nil { 1099 t.Fatalf("Error adding commit: %v", err) 1100 } 1101 if err := lg.Checkout("o", "r", defaultBranch); err != nil { 1102 t.Fatalf("Error checking out master: %v", err) 1103 } 1104 oid := githubql.String(fmt.Sprintf("origin/pr-%d", testpr.number)) 1105 var pr PullRequest 1106 pr.Number = githubql.Int(testpr.number) 1107 pr.HeadRefOID = oid 1108 pr.Commits.Nodes = []struct { 1109 Commit Commit 1110 }{{Commit: Commit{OID: oid}}} 1111 pr.Commits.Nodes[0].Commit.Status.Contexts = append(pr.Commits.Nodes[0].Commit.Status.Contexts, Context{State: githubql.StatusStateSuccess}) 1112 if !testpr.success { 1113 pr.Commits.Nodes[0].Commit.Status.Contexts[0].State = githubql.StatusStateFailure 1114 } 1115 sp.prs = append(sp.prs, *CodeReviewCommonFromPullRequest(&pr)) 1116 } 1117 ca := &config.Agent{} 1118 ca.Set(&config.Config{ 1119 ProwConfig: config.ProwConfig{ 1120 Tide: config.Tide{ 1121 BatchSizeLimitMap: map[string]int{"*": 5}, 1122 }, 1123 }, 1124 JobConfig: config.JobConfig{ 1125 PresubmitsStatic: map[string][]config.Presubmit{ 1126 "o/r": {{ 1127 AlwaysRun: true, 1128 JobBase: config.JobBase{ 1129 Name: "my-presubmit", 1130 }, 1131 }}, 1132 }, 1133 }, 1134 }) 1135 logger := logrus.WithField("component", "tide") 1136 ghProvider := &GitHubProvider{cfg: ca.Config, gc: gc, mergeChecker: newMergeChecker(ca.Config, &fgc{}), logger: logger} 1137 c := &syncController{ 1138 logger: logger, 1139 provider: ghProvider, 1140 config: ca.Config, 1141 pickNewBatch: pickNewBatch(gc, ca.Config, ghProvider), 1142 } 1143 prs, presubmits, err := c.pickBatch(sp, map[int]contextChecker{ 1144 0: &config.TideContextPolicy{}, 1145 1: &config.TideContextPolicy{}, 1146 2: &config.TideContextPolicy{}, 1147 3: &config.TideContextPolicy{}, 1148 4: &config.TideContextPolicy{}, 1149 // Test if scoping of ContextPolicy works correctly 1150 5: &config.TideContextPolicy{RequiredContexts: []string{"context-from-context-checker"}}, 1151 6: &config.TideContextPolicy{}, 1152 7: &config.TideContextPolicy{}, 1153 8: &config.TideContextPolicy{}, 1154 }, c.pickNewBatch) 1155 if err != nil { 1156 t.Fatalf("Error from pickBatch: %v", err) 1157 } 1158 if !apiequality.Semantic.DeepEqual(presubmits, ca.Config().PresubmitsStatic["o/r"]) { 1159 t.Errorf("resolving presubmits failed, diff:\n%v\n", diff.ObjectReflectDiff(presubmits, ca.Config().PresubmitsStatic["o/r"])) 1160 } 1161 for _, testpr := range testprs { 1162 var found bool 1163 for _, pr := range prs { 1164 if int(pr.Number) == testpr.number { 1165 found = true 1166 break 1167 } 1168 } 1169 if found && !testpr.included { 1170 t.Errorf("PR %d should not be picked.", testpr.number) 1171 } else if !found && testpr.included { 1172 t.Errorf("PR %d should be picked.", testpr.number) 1173 } 1174 } 1175 } 1176 1177 func TestMergeMethodCheckerAndPRMergeMethod(t *testing.T) { 1178 squashLabel := "tide/squash" 1179 mergeLabel := "tide/merge" 1180 rebaseLabel := "tide/rebase" 1181 1182 tideConfig := config.Tide{ 1183 TideGitHubConfig: config.TideGitHubConfig{ 1184 SquashLabel: squashLabel, 1185 MergeLabel: mergeLabel, 1186 RebaseLabel: rebaseLabel, 1187 1188 MergeType: map[string]config.TideOrgMergeType{ 1189 "o/configured-rebase": {MergeType: types.MergeRebase}, // GH client allows merge, rebase 1190 "o/configured-squash-allow-rebase": {MergeType: types.MergeSquash}, // GH client allows merge, squash, rebase 1191 "o/configure-re-base": {MergeType: types.MergeRebase}, // GH client allows merge 1192 }, 1193 }, 1194 } 1195 cfg := func() *config.Config { return &config.Config{ProwConfig: config.ProwConfig{Tide: tideConfig}} } 1196 mmc := newMergeChecker(cfg, &fgc{}) 1197 1198 testcases := []struct { 1199 name string 1200 repo string 1201 labels []string 1202 conflict bool 1203 expectedMethod types.PullRequestMergeType 1204 expectErr bool 1205 expectConflictErr bool 1206 }{ 1207 { 1208 name: "default method without PR label override", 1209 repo: "foo", 1210 expectedMethod: types.MergeMerge, 1211 }, 1212 { 1213 name: "irrelevant PR labels ignored", 1214 repo: "foo", 1215 labels: []string{"unrelated"}, 1216 expectedMethod: types.MergeMerge, 1217 }, 1218 { 1219 name: "default method overridden by a PR label", 1220 repo: "allow-squash-nomerge", 1221 labels: []string{"tide/squash"}, 1222 expectedMethod: types.MergeSquash, 1223 }, 1224 { 1225 name: "use method configured for repo in tide config", 1226 repo: "configured-squash-allow-rebase", 1227 labels: []string{"unrelated"}, 1228 expectedMethod: types.MergeSquash, 1229 }, 1230 { 1231 name: "tide config method overridden by a PR label", 1232 repo: "configured-squash-allow-rebase", 1233 labels: []string{"unrelated", "tide/rebase"}, 1234 expectedMethod: types.MergeRebase, 1235 }, 1236 { 1237 name: "multiple merge method PR labels should not merge", 1238 repo: "foo", 1239 labels: []string{"tide/squash", "tide/rebase"}, 1240 expectErr: true, 1241 }, 1242 { 1243 name: "merge conflict", 1244 repo: "foo", 1245 labels: []string{"unrelated"}, 1246 conflict: true, 1247 expectedMethod: types.MergeMerge, 1248 expectErr: false, 1249 expectConflictErr: true, 1250 }, 1251 { 1252 name: "squash label conflicts with merge only GH settings", 1253 repo: "foo", 1254 labels: []string{"tide/squash"}, 1255 expectedMethod: types.MergeSquash, 1256 expectErr: false, 1257 expectConflictErr: true, 1258 }, 1259 { 1260 name: "rebase method tide config conflicts with merge only GH settings", 1261 repo: "configure-re-base", 1262 labels: []string{"unrelated"}, 1263 expectedMethod: types.MergeRebase, 1264 expectErr: false, 1265 expectConflictErr: true, 1266 }, 1267 { 1268 name: "default method conflicts with squash only GH settings", 1269 repo: "squash-nomerge", 1270 labels: []string{"unrelated"}, 1271 expectedMethod: types.MergeMerge, 1272 expectErr: false, 1273 expectConflictErr: true, 1274 }, 1275 } 1276 1277 for _, tc := range testcases { 1278 t.Run(tc.name, func(t *testing.T) { 1279 pr := &PullRequest{ 1280 Repository: struct { 1281 Name githubql.String 1282 NameWithOwner githubql.String 1283 Owner struct { 1284 Login githubql.String 1285 } 1286 }{ 1287 Name: githubql.String(tc.repo), 1288 Owner: struct { 1289 Login githubql.String 1290 }{ 1291 Login: githubql.String("o"), 1292 }, 1293 }, 1294 Labels: struct { 1295 Nodes []struct{ Name githubql.String } 1296 }{ 1297 Nodes: []struct{ Name githubql.String }{}, 1298 }, 1299 CanBeRebased: true, 1300 } 1301 for _, label := range tc.labels { 1302 labelNode := struct{ Name githubql.String }{Name: githubql.String(label)} 1303 pr.Labels.Nodes = append(pr.Labels.Nodes, labelNode) 1304 } 1305 if tc.conflict { 1306 pr.Mergeable = githubql.MergeableStateConflicting 1307 } 1308 1309 actual := mmc.prMergeMethod(tideConfig, CodeReviewCommonFromPullRequest(pr)) 1310 if actual == nil { 1311 if !tc.expectErr { 1312 t.Errorf("multiple merge methods are not allowed") 1313 } 1314 return 1315 } else if tc.expectErr { 1316 t.Errorf("missing expected error") 1317 return 1318 } 1319 if tc.expectedMethod != *actual { 1320 t.Errorf("wanted: %q, got: %q", tc.expectedMethod, *actual) 1321 } 1322 reason, err := mmc.isAllowedToMerge(CodeReviewCommonFromPullRequest(pr)) 1323 if err != nil { 1324 t.Errorf("unexpected processing error: %v", err) 1325 } else if reason != "" { 1326 if !tc.expectConflictErr { 1327 t.Errorf("unexpected merge method conflict error: %v", err) 1328 } 1329 return 1330 } else if tc.expectConflictErr { 1331 t.Errorf("missing expected merge method conflict error") 1332 return 1333 } 1334 }) 1335 } 1336 } 1337 1338 func TestRebaseMergeMethodIsAllowed(t *testing.T) { 1339 orgName := "fake-org" 1340 repoName := "fake-repo" 1341 tideConfig := config.Tide{ 1342 TideGitHubConfig: config.TideGitHubConfig{ 1343 MergeType: map[string]config.TideOrgMergeType{ 1344 fmt.Sprintf("%s/%s", orgName, repoName): {MergeType: types.MergeRebase}, 1345 }, 1346 }, 1347 } 1348 cfg := func() *config.Config { return &config.Config{ProwConfig: config.ProwConfig{Tide: tideConfig}} } 1349 mmc := newMergeChecker(cfg, &fgc{}) 1350 mmc.cache = map[config.OrgRepo]map[types.PullRequestMergeType]bool{ 1351 {Org: orgName, Repo: repoName}: { 1352 types.MergeRebase: true, 1353 }, 1354 } 1355 1356 testCases := []struct { 1357 name string 1358 expectedMergeOutput string 1359 prCanBeRebased bool 1360 }{ 1361 { 1362 name: "Merging PR using rebase successfully", 1363 expectedMergeOutput: "", 1364 prCanBeRebased: true, 1365 }, 1366 { 1367 name: "Merging PR using rebase but it is not allowed", 1368 expectedMergeOutput: "PR can't be rebased", 1369 prCanBeRebased: false, 1370 }, 1371 } 1372 1373 for _, tc := range testCases { 1374 t.Run(tc.name, func(t *testing.T) { 1375 pr := &PullRequest{ 1376 Repository: struct { 1377 Name githubql.String 1378 NameWithOwner githubql.String 1379 Owner struct { 1380 Login githubql.String 1381 } 1382 }{ 1383 Name: githubql.String(repoName), 1384 Owner: struct { 1385 Login githubql.String 1386 }{ 1387 Login: githubql.String(orgName), 1388 }, 1389 }, 1390 Labels: struct { 1391 Nodes []struct{ Name githubql.String } 1392 }{ 1393 Nodes: []struct{ Name githubql.String }{}, 1394 }, 1395 CanBeRebased: githubql.Boolean(tc.prCanBeRebased), 1396 } 1397 1398 mergeOutput, err := mmc.isAllowedToMerge(CodeReviewCommonFromPullRequest(pr)) 1399 if err != nil { 1400 t.Errorf("unexpected error: %v", err) 1401 } else { 1402 if mergeOutput != tc.expectedMergeOutput { 1403 t.Errorf("Expected merge output \"%s\" but got \"%s\"\n", tc.expectedMergeOutput, mergeOutput) 1404 } 1405 } 1406 }) 1407 } 1408 } 1409 1410 func TestTakeActionV2(t *testing.T) { 1411 testTakeAction(localgit.NewV2, t) 1412 } 1413 1414 func testTakeAction(clients localgit.Clients, t *testing.T) { 1415 sleep = func(time.Duration) {} 1416 defer func() { sleep = time.Sleep }() 1417 1418 // PRs 0-9 exist. All are mergable, and all are passing tests. 1419 testcases := []struct { 1420 name string 1421 1422 batchPending bool 1423 successes []int 1424 pendings []int 1425 nones []int 1426 batchMerges []int 1427 presubmits map[int][]config.Presubmit 1428 preExistingJobs []runtime.Object 1429 mergeErrs map[int]error 1430 enableScheduling bool 1431 1432 merged int 1433 triggered int 1434 triggeredBatches int 1435 action Action 1436 }{ 1437 { 1438 name: "no prs to test, should do nothing", 1439 1440 batchPending: true, 1441 successes: []int{}, 1442 pendings: []int{}, 1443 nones: []int{}, 1444 batchMerges: []int{}, 1445 presubmits: map[int][]config.Presubmit{ 1446 100: { 1447 {Reporter: config.Reporter{Context: "foo"}}, 1448 {Reporter: config.Reporter{Context: "if-changed"}}, 1449 }, 1450 }, 1451 merged: 0, 1452 triggered: 0, 1453 action: Wait, 1454 }, 1455 { 1456 name: "pending batch, pending serial, nothing to do", 1457 1458 batchPending: true, 1459 successes: []int{}, 1460 pendings: []int{1}, 1461 nones: []int{0, 2}, 1462 batchMerges: []int{}, 1463 presubmits: map[int][]config.Presubmit{ 1464 100: { 1465 {Reporter: config.Reporter{Context: "foo"}}, 1466 {Reporter: config.Reporter{Context: "if-changed"}}, 1467 }, 1468 }, 1469 merged: 0, 1470 triggered: 0, 1471 action: Wait, 1472 }, 1473 { 1474 name: "pending batch, successful serial, nothing to do", 1475 1476 batchPending: true, 1477 successes: []int{1}, 1478 pendings: []int{}, 1479 nones: []int{0, 2}, 1480 batchMerges: []int{}, 1481 presubmits: map[int][]config.Presubmit{ 1482 100: { 1483 {Reporter: config.Reporter{Context: "foo"}}, 1484 {Reporter: config.Reporter{Context: "if-changed"}}, 1485 }, 1486 }, 1487 merged: 0, 1488 triggered: 0, 1489 action: Wait, 1490 }, 1491 { 1492 name: "pending batch, should trigger serial", 1493 1494 batchPending: true, 1495 successes: []int{}, 1496 pendings: []int{}, 1497 nones: []int{0, 1, 2}, 1498 batchMerges: []int{}, 1499 presubmits: map[int][]config.Presubmit{ 1500 100: { 1501 {Reporter: config.Reporter{Context: "foo"}}, 1502 {Reporter: config.Reporter{Context: "if-changed"}}, 1503 }, 1504 }, 1505 merged: 0, 1506 triggered: 1, 1507 action: Trigger, 1508 }, 1509 { 1510 name: "no pending batch, should trigger batch", 1511 1512 batchPending: false, 1513 successes: []int{}, 1514 pendings: []int{0}, 1515 nones: []int{1, 2, 3}, 1516 batchMerges: []int{}, 1517 presubmits: map[int][]config.Presubmit{ 1518 100: { 1519 {Reporter: config.Reporter{Context: "foo"}}, 1520 {Reporter: config.Reporter{Context: "if-changed"}}, 1521 }, 1522 }, 1523 merged: 0, 1524 triggered: 2, 1525 triggeredBatches: 2, 1526 action: TriggerBatch, 1527 }, 1528 { 1529 name: "one PR, should not trigger batch", 1530 1531 batchPending: false, 1532 successes: []int{}, 1533 pendings: []int{}, 1534 nones: []int{0}, 1535 batchMerges: []int{}, 1536 presubmits: map[int][]config.Presubmit{ 1537 100: { 1538 {Reporter: config.Reporter{Context: "foo"}}, 1539 {Reporter: config.Reporter{Context: "if-changed"}}, 1540 }, 1541 }, 1542 merged: 0, 1543 triggered: 1, 1544 action: Trigger, 1545 }, 1546 { 1547 name: "successful PR, should merge", 1548 1549 batchPending: false, 1550 successes: []int{0}, 1551 pendings: []int{}, 1552 nones: []int{1, 2, 3}, 1553 batchMerges: []int{}, 1554 presubmits: map[int][]config.Presubmit{ 1555 100: { 1556 {Reporter: config.Reporter{Context: "foo"}}, 1557 {Reporter: config.Reporter{Context: "if-changed"}}, 1558 }, 1559 }, 1560 merged: 1, 1561 triggered: 0, 1562 action: Merge, 1563 }, 1564 { 1565 name: "successful batch, should merge", 1566 1567 batchPending: false, 1568 successes: []int{0, 1}, 1569 pendings: []int{2, 3}, 1570 nones: []int{4, 5}, 1571 batchMerges: []int{6, 7, 8}, 1572 presubmits: map[int][]config.Presubmit{ 1573 100: { 1574 {Reporter: config.Reporter{Context: "foo"}}, 1575 {Reporter: config.Reporter{Context: "if-changed"}}, 1576 }, 1577 }, 1578 merged: 3, 1579 triggered: 0, 1580 action: MergeBatch, 1581 }, 1582 { 1583 name: "one PR that triggers RunIfChangedJob", 1584 1585 batchPending: false, 1586 successes: []int{}, 1587 pendings: []int{}, 1588 nones: []int{100}, 1589 batchMerges: []int{}, 1590 presubmits: map[int][]config.Presubmit{ 1591 100: { 1592 {Reporter: config.Reporter{Context: "foo"}}, 1593 {Reporter: config.Reporter{Context: "if-changed"}}, 1594 }, 1595 }, 1596 merged: 0, 1597 triggered: 2, 1598 action: Trigger, 1599 }, 1600 { 1601 name: "no presubmits, merge", 1602 1603 batchPending: false, 1604 successes: []int{5, 4}, 1605 pendings: []int{}, 1606 nones: []int{}, 1607 batchMerges: []int{}, 1608 1609 merged: 1, 1610 triggered: 0, 1611 action: Merge, 1612 }, 1613 { 1614 name: "no presubmits, wait", 1615 1616 batchPending: false, 1617 successes: []int{}, 1618 pendings: []int{}, 1619 nones: []int{}, 1620 batchMerges: []int{}, 1621 1622 merged: 0, 1623 triggered: 0, 1624 action: Wait, 1625 }, 1626 { 1627 name: "no pending serial or batch, should trigger batch", 1628 1629 batchPending: false, 1630 successes: []int{}, 1631 pendings: []int{}, 1632 nones: []int{1, 2, 3}, 1633 batchMerges: []int{}, 1634 presubmits: map[int][]config.Presubmit{ 1635 100: { 1636 {Reporter: config.Reporter{Context: "foo"}}, 1637 {Reporter: config.Reporter{Context: "if-changed"}}, 1638 }, 1639 }, 1640 merged: 0, 1641 triggered: 2, 1642 triggeredBatches: 2, 1643 action: TriggerBatch, 1644 }, 1645 { 1646 name: "no pending serial or batch, should trigger batch and omit pre-existing running job", 1647 1648 batchPending: false, 1649 successes: []int{}, 1650 pendings: []int{}, 1651 nones: []int{1, 2, 3}, 1652 batchMerges: []int{}, 1653 presubmits: map[int][]config.Presubmit{ 1654 100: { 1655 {Reporter: config.Reporter{Context: "foo"}}, 1656 {Reporter: config.Reporter{Context: "if-changed"}}, 1657 }, 1658 }, 1659 preExistingJobs: []runtime.Object{&prowapi.ProwJob{ 1660 ObjectMeta: metav1.ObjectMeta{Name: "my-job", Namespace: "pj-ns"}, 1661 Spec: prowapi.ProwJobSpec{ 1662 Job: "bar", 1663 Type: prowapi.BatchJob, 1664 Refs: &prowapi.Refs{ 1665 Org: "o", 1666 Repo: "r", 1667 BaseRef: defaultBranch, 1668 BaseSHA: defaultBranch, 1669 Pulls: []prowapi.Pull{ 1670 {Number: 1, SHA: "origin/pr-1"}, 1671 {Number: 3, SHA: "origin/pr-3"}, 1672 {Number: 2, SHA: "origin/pr-2"}, 1673 }, 1674 }, 1675 }, 1676 }}, 1677 merged: 0, 1678 triggered: 1, 1679 triggeredBatches: 1, 1680 action: TriggerBatch, 1681 }, 1682 { 1683 name: "no pending serial or batch, should trigger batch and omit pre-existing success job", 1684 1685 batchPending: false, 1686 successes: []int{}, 1687 pendings: []int{}, 1688 nones: []int{1, 2, 3}, 1689 batchMerges: []int{}, 1690 presubmits: map[int][]config.Presubmit{ 1691 100: { 1692 {Reporter: config.Reporter{Context: "foo"}}, 1693 {Reporter: config.Reporter{Context: "if-changed"}}, 1694 }, 1695 }, 1696 preExistingJobs: []runtime.Object{&prowapi.ProwJob{ 1697 ObjectMeta: metav1.ObjectMeta{Name: "my-job", Namespace: "pj-ns"}, 1698 Spec: prowapi.ProwJobSpec{ 1699 Job: "bar", 1700 Type: prowapi.BatchJob, 1701 Refs: &prowapi.Refs{ 1702 Org: "o", 1703 Repo: "r", 1704 BaseRef: defaultBranch, 1705 BaseSHA: defaultBranch, 1706 Pulls: []prowapi.Pull{ 1707 {Number: 1, SHA: "origin/pr-1"}, 1708 {Number: 3, SHA: "origin/pr-3"}, 1709 {Number: 2, SHA: "origin/pr-2"}, 1710 }, 1711 }, 1712 }, 1713 Status: prowapi.ProwJobStatus{ 1714 State: prowapi.SuccessState, 1715 CompletionTime: &metav1.Time{Time: time.Unix(10, 0)}, 1716 }, 1717 }}, 1718 merged: 0, 1719 triggered: 1, 1720 triggeredBatches: 1, 1721 action: TriggerBatch, 1722 }, 1723 { 1724 name: "no pending serial or batch, should trigger batch and ignore pre-existing failure job", 1725 1726 batchPending: false, 1727 successes: []int{}, 1728 pendings: []int{}, 1729 nones: []int{1, 2, 3}, 1730 batchMerges: []int{}, 1731 presubmits: map[int][]config.Presubmit{ 1732 100: { 1733 {Reporter: config.Reporter{Context: "foo"}}, 1734 {Reporter: config.Reporter{Context: "if-changed"}}, 1735 }, 1736 }, 1737 preExistingJobs: []runtime.Object{&prowapi.ProwJob{ 1738 ObjectMeta: metav1.ObjectMeta{Name: "my-job", Namespace: "pj-ns"}, 1739 Spec: prowapi.ProwJobSpec{ 1740 Job: "bar", 1741 Type: prowapi.BatchJob, 1742 Refs: &prowapi.Refs{ 1743 Org: "o", 1744 Repo: "r", 1745 BaseRef: defaultBranch, 1746 BaseSHA: defaultBranch, 1747 Pulls: []prowapi.Pull{ 1748 {Number: 1, SHA: "origin/pr-1"}, 1749 {Number: 3, SHA: "origin/pr-3"}, 1750 {Number: 2, SHA: "origin/pr-2"}, 1751 }, 1752 }, 1753 }, 1754 Status: prowapi.ProwJobStatus{ 1755 State: prowapi.FailureState, 1756 CompletionTime: &metav1.Time{Time: time.Unix(10, 0)}, 1757 }, 1758 }}, 1759 merged: 0, 1760 triggered: 2, 1761 triggeredBatches: 2, 1762 action: TriggerBatch, 1763 }, 1764 { 1765 name: "pending batch, no serial, should trigger serial", 1766 1767 batchPending: true, 1768 successes: []int{}, 1769 pendings: []int{}, 1770 nones: []int{1, 2, 3}, 1771 batchMerges: []int{}, 1772 presubmits: map[int][]config.Presubmit{ 1773 100: { 1774 {Reporter: config.Reporter{Context: "foo"}}, 1775 {Reporter: config.Reporter{Context: "if-changed"}}, 1776 }, 1777 }, 1778 merged: 0, 1779 triggered: 1, 1780 action: Trigger, 1781 }, 1782 { 1783 name: "batch merge errors but continues if a PR is unmergeable", 1784 1785 batchMerges: []int{1, 2, 3}, 1786 mergeErrs: map[int]error{2: github.UnmergablePRError("test error")}, 1787 merged: 2, 1788 triggered: 0, 1789 action: MergeBatch, 1790 }, 1791 { 1792 name: "batch merge errors but continues if a PR has changed", 1793 1794 batchMerges: []int{1, 2, 3}, 1795 mergeErrs: map[int]error{2: github.ModifiedHeadError("test error")}, 1796 merged: 2, 1797 triggered: 0, 1798 action: MergeBatch, 1799 }, 1800 { 1801 name: "batch merge errors but continues on unknown error", 1802 1803 batchMerges: []int{1, 2, 3}, 1804 mergeErrs: map[int]error{2: errors.New("test error")}, 1805 merged: 2, 1806 triggered: 0, 1807 action: MergeBatch, 1808 }, 1809 { 1810 name: "batch merge stops on auth error", 1811 1812 batchMerges: []int{1, 2, 3}, 1813 mergeErrs: map[int]error{2: github.UnauthorizedToPushError("test error")}, 1814 merged: 1, 1815 triggered: 0, 1816 action: MergeBatch, 1817 }, 1818 { 1819 name: "batch merge stops on invalid merge method error", 1820 1821 batchMerges: []int{1, 2, 3}, 1822 mergeErrs: map[int]error{2: github.MergeCommitsForbiddenError("test error")}, 1823 merged: 1, 1824 triggered: 0, 1825 action: MergeBatch, 1826 }, 1827 { 1828 name: "pending batch, should trigger serial in scheduling state", 1829 1830 batchPending: true, 1831 successes: []int{}, 1832 pendings: []int{}, 1833 nones: []int{0, 1, 2}, 1834 batchMerges: []int{}, 1835 presubmits: map[int][]config.Presubmit{ 1836 100: { 1837 {Reporter: config.Reporter{Context: "foo"}}, 1838 {Reporter: config.Reporter{Context: "if-changed"}}, 1839 }, 1840 }, 1841 merged: 0, 1842 triggered: 1, 1843 action: Trigger, 1844 enableScheduling: true, 1845 }, 1846 } 1847 1848 for _, tc := range testcases { 1849 t.Run(tc.name, func(t *testing.T) { 1850 ca := &config.Agent{} 1851 pjNamespace := "pj-ns" 1852 cfg := &config.Config{ 1853 ProwConfig: config.ProwConfig{ 1854 ProwJobNamespace: pjNamespace, 1855 Scheduler: config.Scheduler{Enabled: tc.enableScheduling}, 1856 }, 1857 } 1858 if err := cfg.SetPresubmits( 1859 map[string][]config.Presubmit{ 1860 "o/r": { 1861 { 1862 Reporter: config.Reporter{Context: "foo"}, 1863 Trigger: "/test all", 1864 RerunCommand: "/test all", 1865 AlwaysRun: true, 1866 }, 1867 { 1868 JobBase: config.JobBase{ 1869 Name: "bar", 1870 }, 1871 Reporter: config.Reporter{Context: "bar"}, 1872 Trigger: "/test bar", 1873 RerunCommand: "/test bar", 1874 AlwaysRun: true, 1875 }, 1876 { 1877 Reporter: config.Reporter{Context: "if-changed"}, 1878 Trigger: "/test if-changed", 1879 RerunCommand: "/test if-changed", 1880 RegexpChangeMatcher: config.RegexpChangeMatcher{ 1881 RunIfChanged: "CHANGED", 1882 }, 1883 }, 1884 { 1885 Reporter: config.Reporter{Context: "if-changed"}, 1886 Trigger: "/test if-changed", 1887 RerunCommand: "/test if-changed", 1888 RegexpChangeMatcher: config.RegexpChangeMatcher{ 1889 SkipIfOnlyChanged: "CHANGED1", 1890 }, 1891 }, 1892 }, 1893 }, 1894 ); err != nil { 1895 t.Fatalf("failed to set presubmits: %v", err) 1896 } 1897 ca.Set(cfg) 1898 if len(tc.presubmits) > 0 { 1899 for i := 0; i <= 8; i++ { 1900 tc.presubmits[i] = []config.Presubmit{{Reporter: config.Reporter{Context: "foo"}}} 1901 } 1902 } 1903 lg, gc, err := clients() 1904 if err != nil { 1905 t.Fatalf("Error making local git: %v", err) 1906 } 1907 defer gc.Clean() 1908 defer lg.Clean() 1909 if err := lg.MakeFakeRepo("o", "r"); err != nil { 1910 t.Fatalf("Error making fake repo: %v", err) 1911 } 1912 if err := lg.AddCommit("o", "r", map[string][]byte{"foo": []byte("foo")}); err != nil { 1913 t.Fatalf("Adding initial commit: %v", err) 1914 } 1915 1916 sp := subpool{ 1917 log: logrus.WithField("component", "tide"), 1918 presubmits: tc.presubmits, 1919 cc: map[int]contextChecker{ 1920 0: &config.TideContextPolicy{}, 1921 1: &config.TideContextPolicy{}, 1922 2: &config.TideContextPolicy{}, 1923 3: &config.TideContextPolicy{}, 1924 4: &config.TideContextPolicy{}, 1925 5: &config.TideContextPolicy{}, 1926 6: &config.TideContextPolicy{}, 1927 7: &config.TideContextPolicy{}, 1928 8: &config.TideContextPolicy{}, 1929 100: &config.TideContextPolicy{}, 1930 }, 1931 org: "o", 1932 repo: "r", 1933 branch: defaultBranch, 1934 sha: defaultBranch, 1935 } 1936 genPulls := func(nums []int) []CodeReviewCommon { 1937 var prs []CodeReviewCommon 1938 for _, i := range nums { 1939 if err := lg.CheckoutNewBranch("o", "r", fmt.Sprintf("pr-%d", i)); err != nil { 1940 t.Fatalf("Error checking out new branch: %v", err) 1941 } 1942 if err := lg.AddCommit("o", "r", map[string][]byte{fmt.Sprintf("%d", i): []byte("WOW")}); err != nil { 1943 t.Fatalf("Error adding commit: %v", err) 1944 } 1945 if err := lg.Checkout("o", "r", defaultBranch); err != nil { 1946 t.Fatalf("Error checking out master: %v", err) 1947 } 1948 oid := githubql.String(fmt.Sprintf("origin/pr-%d", i)) 1949 var pr PullRequest 1950 pr.Number = githubql.Int(i) 1951 pr.HeadRefOID = oid 1952 pr.Commits.Nodes = []struct { 1953 Commit Commit 1954 }{{Commit: Commit{OID: oid}}} 1955 sp.prs = append(sp.prs, *CodeReviewCommonFromPullRequest(&pr)) 1956 prs = append(prs, *CodeReviewCommonFromPullRequest(&pr)) 1957 } 1958 return prs 1959 } 1960 fgc := fgc{mergeErrs: tc.mergeErrs} 1961 log := logrus.WithField("controller", "tide") 1962 ghProvider := newGitHubProvider(log, &fgc, gc, ca.Config, nil, false) 1963 c, err := newSyncController( 1964 context.Background(), 1965 log, 1966 newFakeManager(tc.preExistingJobs...), 1967 ghProvider, 1968 ca.Config, 1969 gc, 1970 nil, 1971 false, 1972 &statusUpdate{ 1973 dontUpdateStatus: &threadSafePRSet{}, 1974 newPoolPending: make(chan bool), 1975 }, 1976 ) 1977 if err != nil { 1978 t.Fatalf("failed to construct sync controller: %v", err) 1979 } 1980 c.changedFiles = &changedFilesAgent{ 1981 provider: ghProvider, 1982 nextChangeCache: make(map[changeCacheKey][]string), 1983 } 1984 var batchPending []CodeReviewCommon 1985 if tc.batchPending { 1986 batchPending = []CodeReviewCommon{{}} 1987 } 1988 if act, _, _ := c.takeAction(sp, batchPending, genPulls(tc.successes), genPulls(tc.pendings), genPulls(tc.nones), genPulls(tc.batchMerges), sp.presubmits); act != tc.action { 1989 t.Errorf("Wrong action. Got %v, wanted %v.", act, tc.action) 1990 } 1991 1992 prowJobs := &prowapi.ProwJobList{} 1993 if err := c.prowJobClient.List(context.Background(), prowJobs); err != nil { 1994 t.Fatalf("failed to list ProwJobs: %v", err) 1995 } 1996 var filteredProwJobs []prowapi.ProwJob 1997 // Filter out the ones we passed in 1998 for _, job := range prowJobs.Items { 1999 var preExists bool 2000 for _, preExistingJob := range tc.preExistingJobs { 2001 if reflect.DeepEqual(*preExistingJob.(*prowapi.ProwJob), job) { 2002 preExists = true 2003 } 2004 } 2005 if !preExists { 2006 filteredProwJobs = append(filteredProwJobs, job) 2007 } 2008 2009 } 2010 numCreated := len(filteredProwJobs) 2011 2012 var batchJobs []*prowapi.ProwJob 2013 for _, pj := range filteredProwJobs { 2014 if pj.Namespace != pjNamespace { 2015 t.Errorf("prowjob %q didn't have expected namespace %q but %q", pj.Name, pjNamespace, pj.Namespace) 2016 } 2017 if pj.Spec.Type == prowapi.BatchJob { 2018 pj := pj 2019 batchJobs = append(batchJobs, &pj) 2020 } 2021 } 2022 2023 if tc.triggered != numCreated { 2024 t.Errorf("Wrong number of jobs triggered. Got %d, expected %d.", numCreated, tc.triggered) 2025 } 2026 if tc.triggered > 0 && tc.enableScheduling { 2027 for _, pj := range filteredProwJobs { 2028 if pj.Status.State != prowapi.SchedulingState { 2029 t.Errorf("Wrong ProwJob state. Got %s, expected %s.", pj.Status.State, prowapi.SchedulingState) 2030 } 2031 } 2032 } 2033 if tc.merged != fgc.merged { 2034 t.Errorf("Wrong number of merges. Got %d, expected %d.", fgc.merged, tc.merged) 2035 } 2036 if n := len(c.statusUpdate.dontUpdateStatus.data); n != tc.merged+len(tc.mergeErrs) { 2037 t.Errorf("expected %d entries in the dontUpdateStatus map, got %d", tc.merged+len(tc.mergeErrs), n) 2038 } 2039 // Ensure that the correct number of batch jobs were triggered 2040 if tc.triggeredBatches != len(batchJobs) { 2041 t.Errorf("Wrong number of batches triggered. Got %d, expected %d.", len(batchJobs), tc.triggeredBatches) 2042 } 2043 for _, job := range batchJobs { 2044 if len(job.Spec.Refs.Pulls) <= 1 { 2045 t.Error("Found a batch job that doesn't contain multiple pull refs!") 2046 } 2047 } 2048 }) 2049 } 2050 } 2051 2052 func TestServeHTTP(t *testing.T) { 2053 pr1 := PullRequest{} 2054 pr1.Commits.Nodes = append(pr1.Commits.Nodes, struct{ Commit Commit }{}) 2055 pr1.Commits.Nodes[0].Commit.Status.Contexts = []Context{{ 2056 Context: githubql.String("coverage/coveralls"), 2057 Description: githubql.String("Coverage increased (+0.1%) to 27.599%"), 2058 }} 2059 hist, err := history.New(100, nil, "") 2060 if err != nil { 2061 t.Fatalf("Failed to create history client: %v", err) 2062 } 2063 cfg := func() *config.Config { return &config.Config{} } 2064 c := &syncController{ 2065 pools: []Pool{ 2066 { 2067 MissingPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&pr1)}, 2068 Action: Merge, 2069 }, 2070 }, 2071 provider: &GitHubProvider{ 2072 mergeChecker: newMergeChecker(cfg, &fgc{}), 2073 }, 2074 History: hist, 2075 } 2076 s := httptest.NewServer(c) 2077 defer s.Close() 2078 resp, err := http.Get(s.URL) 2079 if err != nil { 2080 t.Errorf("GET error: %v", err) 2081 } 2082 defer resp.Body.Close() 2083 var pools []Pool 2084 if err := json.NewDecoder(resp.Body).Decode(&pools); err != nil { 2085 t.Fatalf("JSON decoding error: %v", err) 2086 } 2087 if !reflect.DeepEqual(c.pools, pools) { 2088 t.Errorf("Received pools %v do not match original pools %v.", pools, c.pools) 2089 } 2090 } 2091 2092 func testPR(org, repo, branch string, number int, mergeable githubql.MergeableState) *PullRequest { 2093 pr := PullRequest{ 2094 Number: githubql.Int(number), 2095 Mergeable: mergeable, 2096 HeadRefOID: githubql.String("SHA"), 2097 } 2098 pr.Repository.Owner.Login = githubql.String(org) 2099 pr.Repository.Name = githubql.String(repo) 2100 pr.Repository.NameWithOwner = githubql.String(fmt.Sprintf("%s/%s", org, repo)) 2101 pr.BaseRef.Name = githubql.String(branch) 2102 2103 pr.Commits.Nodes = append(pr.Commits.Nodes, struct{ Commit Commit }{ 2104 Commit{ 2105 Status: struct{ Contexts []Context }{ 2106 Contexts: []Context{ 2107 { 2108 Context: githubql.String("context"), 2109 State: githubql.StatusStateSuccess, 2110 }, 2111 }, 2112 }, 2113 OID: githubql.String("SHA"), 2114 }, 2115 }) 2116 return &pr 2117 } 2118 2119 func testPRWithLabels(org, repo, branch string, number int, mergeable githubql.MergeableState, labels []string) *PullRequest { 2120 pr := testPR(org, repo, branch, number, mergeable) 2121 for _, label := range labels { 2122 labelNode := struct{ Name githubql.String }{Name: githubql.String(label)} 2123 pr.Labels.Nodes = append(pr.Labels.Nodes, labelNode) 2124 } 2125 return pr 2126 } 2127 2128 func TestSync(t *testing.T) { 2129 sleep = func(time.Duration) {} 2130 defer func() { sleep = time.Sleep }() 2131 2132 mergeableA := *testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable) 2133 unmergeableA := *testPR("org", "repo", "A", 6, githubql.MergeableStateConflicting) 2134 unmergeableB := *testPR("org", "repo", "B", 7, githubql.MergeableStateConflicting) 2135 unknownA := *testPR("org", "repo", "A", 8, githubql.MergeableStateUnknown) 2136 2137 testcases := []struct { 2138 name string 2139 prs []PullRequest 2140 2141 expectedPools []Pool 2142 }{ 2143 { 2144 name: "no PRs", 2145 prs: []PullRequest{}, 2146 expectedPools: []Pool{}, 2147 }, 2148 { 2149 name: "1 mergeable PR", 2150 prs: []PullRequest{mergeableA}, 2151 expectedPools: []Pool{{ 2152 Org: "org", 2153 Repo: "repo", 2154 Branch: "A", 2155 SuccessPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)}, 2156 Action: Merge, 2157 Target: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)}, 2158 TenantIDs: []string{}, 2159 }}, 2160 }, 2161 { 2162 name: "1 unmergeable PR", 2163 prs: []PullRequest{unmergeableA}, 2164 expectedPools: []Pool{}, 2165 }, 2166 { 2167 name: "1 unknown PR", 2168 prs: []PullRequest{unknownA}, 2169 expectedPools: []Pool{{ 2170 Org: "org", 2171 Repo: "repo", 2172 Branch: "A", 2173 SuccessPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&unknownA)}, 2174 Action: Merge, 2175 Target: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&unknownA)}, 2176 TenantIDs: []string{}, 2177 }}, 2178 }, 2179 { 2180 name: "1 mergeable, 1 unmergeable (different pools)", 2181 prs: []PullRequest{mergeableA, unmergeableB}, 2182 expectedPools: []Pool{{ 2183 Org: "org", 2184 Repo: "repo", 2185 Branch: "A", 2186 SuccessPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)}, 2187 Action: Merge, 2188 Target: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)}, 2189 TenantIDs: []string{}, 2190 }}, 2191 }, 2192 { 2193 name: "1 mergeable, 1 unmergeable (same pool)", 2194 prs: []PullRequest{mergeableA, unmergeableA}, 2195 expectedPools: []Pool{{ 2196 Org: "org", 2197 Repo: "repo", 2198 Branch: "A", 2199 SuccessPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)}, 2200 Action: Merge, 2201 Target: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)}, 2202 TenantIDs: []string{}, 2203 }}, 2204 }, 2205 { 2206 name: "1 mergeable PR (satisfies multiple queries)", 2207 prs: []PullRequest{mergeableA, mergeableA}, 2208 expectedPools: []Pool{{ 2209 Org: "org", 2210 Repo: "repo", 2211 Branch: "A", 2212 SuccessPRs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)}, 2213 Action: Merge, 2214 Target: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(&mergeableA)}, 2215 TenantIDs: []string{}, 2216 }}, 2217 }, 2218 } 2219 2220 for _, tc := range testcases { 2221 t.Run(tc.name, func(t *testing.T) { 2222 fgc := &fgc{ 2223 prs: map[string][]PullRequest{"": tc.prs}, 2224 refs: map[string]string{ 2225 "org/repo heads/A": "SHA", 2226 "org/repo A": "SHA", 2227 "org/repo heads/B": "SHA", 2228 "org/repo B": "SHA", 2229 }, 2230 } 2231 ca := &config.Agent{} 2232 ca.Set(&config.Config{ 2233 ProwConfig: config.ProwConfig{ 2234 Tide: config.Tide{ 2235 MaxGoroutines: 4, 2236 TideGitHubConfig: config.TideGitHubConfig{ 2237 Queries: []config.TideQuery{{}}, 2238 StatusUpdatePeriod: &metav1.Duration{Duration: time.Second * 0}, 2239 }, 2240 }, 2241 }, 2242 }) 2243 hist, err := history.New(100, nil, "") 2244 if err != nil { 2245 t.Fatalf("Failed to create history client: %v", err) 2246 } 2247 mergeChecker := newMergeChecker(ca.Config, fgc) 2248 sc := &statusController{ 2249 pjClient: fakectrlruntimeclient.NewClientBuilder().Build(), 2250 logger: logrus.WithField("controller", "status-update"), 2251 ghc: fgc, 2252 gc: nil, 2253 config: ca.Config, 2254 shutDown: make(chan bool), 2255 statusUpdate: &statusUpdate{ 2256 dontUpdateStatus: &threadSafePRSet{}, 2257 newPoolPending: make(chan bool), 2258 }, 2259 } 2260 go sc.run() 2261 defer sc.shutdown() 2262 log := logrus.WithField("controller", "sync") 2263 ghProvider := newGitHubProvider(log, fgc, nil, ca.Config, mergeChecker, false) 2264 c := &syncController{ 2265 config: ca.Config, 2266 provider: ghProvider, 2267 prowJobClient: fakectrlruntimeclient.NewClientBuilder().Build(), 2268 logger: log, 2269 changedFiles: &changedFilesAgent{ 2270 provider: ghProvider, 2271 nextChangeCache: make(map[changeCacheKey][]string), 2272 }, 2273 History: hist, 2274 statusUpdate: &statusUpdate{ 2275 dontUpdateStatus: &threadSafePRSet{}, 2276 newPoolPending: make(chan bool), 2277 }, 2278 } 2279 2280 if err := c.Sync(); err != nil { 2281 t.Fatalf("Unexpected error from 'Sync()': %v.", err) 2282 } 2283 if len(tc.expectedPools) != len(c.pools) { 2284 t.Fatalf("Tide pools did not match expected. Got %#v, expected %#v.", c.pools, tc.expectedPools) 2285 } 2286 for _, expected := range tc.expectedPools { 2287 var match *Pool 2288 for i, actual := range c.pools { 2289 if expected.Org == actual.Org && expected.Repo == actual.Repo && expected.Branch == actual.Branch { 2290 match = &c.pools[i] 2291 } 2292 } 2293 if match == nil { 2294 t.Errorf("Failed to find expected pool %s/%s %s.", expected.Org, expected.Repo, expected.Branch) 2295 } else if !reflect.DeepEqual(*match, expected) { 2296 t.Errorf("Expected pool %#v does not match actual pool %#v.", expected, *match) 2297 } 2298 } 2299 }) 2300 } 2301 } 2302 2303 func TestFilterSubpool(t *testing.T) { 2304 presubmits := map[int][]config.Presubmit{ 2305 1: {{Reporter: config.Reporter{Context: "pj-a"}}}, 2306 2: {{Reporter: config.Reporter{Context: "pj-a"}}, {Reporter: config.Reporter{Context: "pj-b"}}}, 2307 } 2308 2309 trueVar := true 2310 cc := map[int]contextChecker{ 2311 1: &config.TideContextPolicy{ 2312 RequiredContexts: []string{"pj-a", "pj-b", "other-a"}, 2313 OptionalContexts: []string{"tide", "pj-c"}, 2314 SkipUnknownContexts: &trueVar, 2315 }, 2316 2: &config.TideContextPolicy{ 2317 RequiredContexts: []string{"pj-a", "pj-b", "other-a"}, 2318 OptionalContexts: []string{"tide", "pj-c"}, 2319 SkipUnknownContexts: &trueVar, 2320 }, 2321 } 2322 2323 type pr struct { 2324 number int 2325 mergeable bool 2326 contexts []Context 2327 checkRuns []CheckRun 2328 } 2329 tcs := []struct { 2330 name string 2331 2332 prs []pr 2333 expectedPRs []int // Empty indicates no subpool should be returned. 2334 }{ 2335 { 2336 name: "one mergeable passing PR (omitting optional context)", 2337 prs: []pr{ 2338 { 2339 number: 1, 2340 mergeable: true, 2341 contexts: []Context{ 2342 { 2343 Context: githubql.String("pj-a"), 2344 State: githubql.StatusStateSuccess, 2345 }, 2346 { 2347 Context: githubql.String("pj-b"), 2348 State: githubql.StatusStateSuccess, 2349 }, 2350 { 2351 Context: githubql.String("other-a"), 2352 State: githubql.StatusStateSuccess, 2353 }, 2354 }, 2355 }, 2356 }, 2357 expectedPRs: []int{1}, 2358 }, 2359 { 2360 name: "one mergeable passing PR (omitting optional context), checkrun is considered", 2361 prs: []pr{ 2362 { 2363 number: 1, 2364 mergeable: true, 2365 contexts: []Context{ 2366 { 2367 Context: githubql.String("pj-a"), 2368 State: githubql.StatusStateSuccess, 2369 }, 2370 { 2371 Context: githubql.String("pj-b"), 2372 State: githubql.StatusStateSuccess, 2373 }, 2374 }, 2375 checkRuns: []CheckRun{{ 2376 Name: githubql.String("other-a"), 2377 Status: githubql.String(githubql.CheckStatusStateCompleted), 2378 Conclusion: githubql.String(githubql.StatusStateSuccess), 2379 }}, 2380 }, 2381 }, 2382 expectedPRs: []int{1}, 2383 }, 2384 { 2385 name: "one mergeable passing PR (omitting optional context), neutral checkrun is considered success", 2386 prs: []pr{ 2387 { 2388 number: 1, 2389 mergeable: true, 2390 contexts: []Context{ 2391 { 2392 Context: githubql.String("pj-a"), 2393 State: githubql.StatusStateSuccess, 2394 }, 2395 { 2396 Context: githubql.String("pj-b"), 2397 State: githubql.StatusStateSuccess, 2398 }, 2399 }, 2400 checkRuns: []CheckRun{{ 2401 Name: githubql.String("other-a"), 2402 Status: githubql.String(githubql.CheckStatusStateCompleted), 2403 Conclusion: githubql.String(githubql.CheckConclusionStateNeutral), 2404 }}, 2405 }, 2406 }, 2407 expectedPRs: []int{1}, 2408 }, 2409 { 2410 name: "Incomplete checkrun throws the pr out", 2411 prs: []pr{ 2412 { 2413 number: 1, 2414 mergeable: true, 2415 contexts: []Context{ 2416 { 2417 Context: githubql.String("pj-a"), 2418 State: githubql.StatusStateSuccess, 2419 }, 2420 { 2421 Context: githubql.String("pj-b"), 2422 State: githubql.StatusStateSuccess, 2423 }, 2424 }, 2425 checkRuns: []CheckRun{{ 2426 Name: githubql.String("other-a"), 2427 Conclusion: githubql.String(githubql.StatusStateSuccess), 2428 }}, 2429 }, 2430 }, 2431 }, 2432 { 2433 name: "Failing checkrun throws the pr out", 2434 prs: []pr{ 2435 { 2436 number: 1, 2437 mergeable: true, 2438 contexts: []Context{ 2439 { 2440 Context: githubql.String("pj-a"), 2441 State: githubql.StatusStateSuccess, 2442 }, 2443 { 2444 Context: githubql.String("pj-b"), 2445 State: githubql.StatusStateSuccess, 2446 }, 2447 }, 2448 checkRuns: []CheckRun{{ 2449 Name: githubql.String("other-a"), 2450 Status: githubql.String(githubql.CheckStatusStateCompleted), 2451 Conclusion: githubql.String(githubql.StatusStateFailure), 2452 }}, 2453 }, 2454 }, 2455 }, 2456 { 2457 name: "one unmergeable passing PR", 2458 prs: []pr{ 2459 { 2460 number: 1, 2461 mergeable: false, 2462 contexts: []Context{ 2463 { 2464 Context: githubql.String("pj-a"), 2465 State: githubql.StatusStateSuccess, 2466 }, 2467 { 2468 Context: githubql.String("pj-b"), 2469 State: githubql.StatusStateSuccess, 2470 }, 2471 { 2472 Context: githubql.String("other-a"), 2473 State: githubql.StatusStateSuccess, 2474 }, 2475 }, 2476 }, 2477 }, 2478 expectedPRs: []int{}, 2479 }, 2480 { 2481 name: "one mergeable PR pending non-PJ context (consider failing)", 2482 prs: []pr{ 2483 { 2484 number: 2, 2485 mergeable: true, 2486 contexts: []Context{ 2487 { 2488 Context: githubql.String("pj-a"), 2489 State: githubql.StatusStateSuccess, 2490 }, 2491 { 2492 Context: githubql.String("pj-b"), 2493 State: githubql.StatusStateSuccess, 2494 }, 2495 { 2496 Context: githubql.String("other-a"), 2497 State: githubql.StatusStatePending, 2498 }, 2499 }, 2500 }, 2501 }, 2502 expectedPRs: []int{}, 2503 }, 2504 { 2505 name: "one mergeable PR pending PJ context (consider in pool)", 2506 prs: []pr{ 2507 { 2508 number: 2, 2509 mergeable: true, 2510 contexts: []Context{ 2511 { 2512 Context: githubql.String("pj-a"), 2513 State: githubql.StatusStateSuccess, 2514 }, 2515 { 2516 Context: githubql.String("pj-b"), 2517 State: githubql.StatusStatePending, 2518 }, 2519 { 2520 Context: githubql.String("other-a"), 2521 State: githubql.StatusStateSuccess, 2522 }, 2523 }, 2524 }, 2525 }, 2526 expectedPRs: []int{2}, 2527 }, 2528 { 2529 name: "one mergeable PR failing PJ context (consider failing)", 2530 prs: []pr{ 2531 { 2532 number: 2, 2533 mergeable: true, 2534 contexts: []Context{ 2535 { 2536 Context: githubql.String("pj-a"), 2537 State: githubql.StatusStateSuccess, 2538 }, 2539 { 2540 Context: githubql.String("pj-b"), 2541 State: githubql.StatusStateFailure, 2542 }, 2543 { 2544 Context: githubql.String("other-a"), 2545 State: githubql.StatusStateSuccess, 2546 }, 2547 }, 2548 }, 2549 }, 2550 expectedPRs: []int{}, 2551 }, 2552 { 2553 name: "one mergeable PR missing PJ context (consider failing)", 2554 prs: []pr{ 2555 { 2556 number: 2, 2557 mergeable: true, 2558 contexts: []Context{ 2559 { 2560 Context: githubql.String("pj-b"), 2561 State: githubql.StatusStateSuccess, 2562 }, 2563 { 2564 Context: githubql.String("other-a"), 2565 State: githubql.StatusStateSuccess, 2566 }, 2567 }, 2568 }, 2569 }, 2570 expectedPRs: []int{}, 2571 }, 2572 { 2573 name: "one mergeable PR failing unknown context (consider in pool)", 2574 prs: []pr{ 2575 { 2576 number: 2, 2577 mergeable: true, 2578 contexts: []Context{ 2579 { 2580 Context: githubql.String("pj-a"), 2581 State: githubql.StatusStateSuccess, 2582 }, 2583 { 2584 Context: githubql.String("pj-b"), 2585 State: githubql.StatusStateSuccess, 2586 }, 2587 { 2588 Context: githubql.String("other-a"), 2589 State: githubql.StatusStateSuccess, 2590 }, 2591 { 2592 Context: githubql.String("unknown"), 2593 State: githubql.StatusStateFailure, 2594 }, 2595 }, 2596 }, 2597 }, 2598 expectedPRs: []int{2}, 2599 }, 2600 { 2601 name: "one PR failing non-PJ required context; one PR successful (should not prune pool)", 2602 prs: []pr{ 2603 { 2604 number: 1, 2605 mergeable: true, 2606 contexts: []Context{ 2607 { 2608 Context: githubql.String("pj-a"), 2609 State: githubql.StatusStateSuccess, 2610 }, 2611 { 2612 Context: githubql.String("pj-b"), 2613 State: githubql.StatusStateSuccess, 2614 }, 2615 { 2616 Context: githubql.String("other-a"), 2617 State: githubql.StatusStateFailure, 2618 }, 2619 }, 2620 }, 2621 { 2622 number: 2, 2623 mergeable: true, 2624 contexts: []Context{ 2625 { 2626 Context: githubql.String("pj-a"), 2627 State: githubql.StatusStateSuccess, 2628 }, 2629 { 2630 Context: githubql.String("pj-b"), 2631 State: githubql.StatusStateSuccess, 2632 }, 2633 { 2634 Context: githubql.String("other-a"), 2635 State: githubql.StatusStateSuccess, 2636 }, 2637 { 2638 Context: githubql.String("unknown"), 2639 State: githubql.StatusStateSuccess, 2640 }, 2641 }, 2642 }, 2643 }, 2644 expectedPRs: []int{2}, 2645 }, 2646 { 2647 name: "two successful PRs", 2648 prs: []pr{ 2649 { 2650 number: 1, 2651 mergeable: true, 2652 contexts: []Context{ 2653 { 2654 Context: githubql.String("pj-a"), 2655 State: githubql.StatusStateSuccess, 2656 }, 2657 { 2658 Context: githubql.String("pj-b"), 2659 State: githubql.StatusStateSuccess, 2660 }, 2661 { 2662 Context: githubql.String("other-a"), 2663 State: githubql.StatusStateSuccess, 2664 }, 2665 }, 2666 }, 2667 { 2668 number: 2, 2669 mergeable: true, 2670 contexts: []Context{ 2671 { 2672 Context: githubql.String("pj-a"), 2673 State: githubql.StatusStateSuccess, 2674 }, 2675 { 2676 Context: githubql.String("pj-b"), 2677 State: githubql.StatusStateSuccess, 2678 }, 2679 { 2680 Context: githubql.String("other-a"), 2681 State: githubql.StatusStateSuccess, 2682 }, 2683 }, 2684 }, 2685 }, 2686 expectedPRs: []int{1, 2}, 2687 }, 2688 } 2689 for _, tc := range tcs { 2690 t.Run(tc.name, func(t *testing.T) { 2691 sp := &subpool{ 2692 org: "org", 2693 repo: "repo", 2694 branch: "branch", 2695 presubmits: presubmits, 2696 cc: cc, 2697 log: logrus.WithFields(logrus.Fields{"org": "org", "repo": "repo", "branch": "branch"}), 2698 } 2699 for _, pull := range tc.prs { 2700 pr := PullRequest{ 2701 Number: githubql.Int(pull.number), 2702 } 2703 var checkRunNodes []CheckRunNode 2704 for _, checkRun := range pull.checkRuns { 2705 checkRunNodes = append(checkRunNodes, CheckRunNode{CheckRun: checkRun}) 2706 } 2707 pr.Commits.Nodes = []struct{ Commit Commit }{ 2708 { 2709 Commit{ 2710 Status: struct{ Contexts []Context }{ 2711 Contexts: pull.contexts, 2712 }, 2713 StatusCheckRollup: StatusCheckRollup{ 2714 Contexts: StatusCheckRollupContext{ 2715 Nodes: checkRunNodes, 2716 }, 2717 }, 2718 }, 2719 }, 2720 } 2721 if !pull.mergeable { 2722 pr.Mergeable = githubql.MergeableStateConflicting 2723 } 2724 sp.prs = append(sp.prs, *CodeReviewCommonFromPullRequest(&pr)) 2725 } 2726 2727 configGetter := func() *config.Config { return &config.Config{} } 2728 mmc := newMergeChecker(configGetter, &fgc{}) 2729 provider := &GitHubProvider{ 2730 cfg: configGetter, 2731 mergeChecker: mmc, 2732 logger: logrus.WithContext(context.Background()), 2733 } 2734 filtered := filterSubpool(provider, mmc.isAllowedToMerge, sp) 2735 if len(tc.expectedPRs) == 0 { 2736 if filtered != nil { 2737 t.Fatalf("Expected subpool to be pruned, but got: %v", filtered) 2738 } 2739 return 2740 } 2741 if filtered == nil { 2742 t.Fatalf("Expected subpool to have %d prs, but it was pruned.", len(tc.expectedPRs)) 2743 } 2744 if got := prNumbers(filtered.prs); !reflect.DeepEqual(got, tc.expectedPRs) { 2745 t.Errorf("Expected filtered pool to have PRs %v, but got %v.", tc.expectedPRs, got) 2746 } 2747 }) 2748 } 2749 } 2750 2751 func TestIsPassing(t *testing.T) { 2752 yes := true 2753 no := false 2754 headSHA := "head" 2755 success := string(githubql.StatusStateSuccess) 2756 failure := string(githubql.StatusStateFailure) 2757 testCases := []struct { 2758 name string 2759 passing bool 2760 config config.TideContextPolicy 2761 combinedContexts map[string]string 2762 availableContexts []string 2763 failedContexts []string 2764 }{ 2765 { 2766 name: "empty policy - success (trust combined status)", 2767 passing: true, 2768 combinedContexts: map[string]string{"c1": success, "c2": success, statusContext: failure}, 2769 availableContexts: []string{"c1", "c2", statusContext}, 2770 }, 2771 { 2772 name: "empty policy - failure because of failed context c4 (trust combined status)", 2773 passing: false, 2774 combinedContexts: map[string]string{"c1": success, "c2": success, "c3": failure, statusContext: failure}, 2775 availableContexts: []string{"c1", "c2", "c3", statusContext}, 2776 failedContexts: []string{"c3"}, 2777 }, 2778 { 2779 name: "passing (trust combined status)", 2780 passing: true, 2781 config: config.TideContextPolicy{ 2782 RequiredContexts: []string{"c1", "c2", "c3"}, 2783 SkipUnknownContexts: &no, 2784 }, 2785 combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, statusContext: failure}, 2786 availableContexts: []string{"c1", "c2", "c3", statusContext}, 2787 }, 2788 { 2789 name: "failing because of missing required check c3", 2790 passing: false, 2791 config: config.TideContextPolicy{ 2792 RequiredContexts: []string{"c1", "c2", "c3"}, 2793 }, 2794 combinedContexts: map[string]string{"c1": success, "c2": success, statusContext: failure}, 2795 availableContexts: []string{"c1", "c2", statusContext}, 2796 failedContexts: []string{"c3"}, 2797 }, 2798 { 2799 name: "failing because of failed context c2", 2800 passing: false, 2801 combinedContexts: map[string]string{"c1": success, "c2": failure}, 2802 config: config.TideContextPolicy{ 2803 RequiredContexts: []string{"c1", "c2", "c3"}, 2804 OptionalContexts: []string{"c4"}, 2805 }, 2806 availableContexts: []string{"c1", "c2"}, 2807 failedContexts: []string{"c2", "c3"}, 2808 }, 2809 { 2810 name: "passing because of failed context c4 is optional", 2811 passing: true, 2812 2813 combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure}, 2814 config: config.TideContextPolicy{ 2815 RequiredContexts: []string{"c1", "c2", "c3"}, 2816 OptionalContexts: []string{"c4"}, 2817 }, 2818 availableContexts: []string{"c1", "c2", "c3", "c4"}, 2819 }, 2820 { 2821 name: "skipping unknown contexts - failing because of missing required context c3", 2822 passing: false, 2823 config: config.TideContextPolicy{ 2824 RequiredContexts: []string{"c1", "c2", "c3"}, 2825 SkipUnknownContexts: &yes, 2826 }, 2827 combinedContexts: map[string]string{"c1": success, "c2": success, statusContext: failure}, 2828 availableContexts: []string{"c1", "c2", statusContext}, 2829 failedContexts: []string{"c3"}, 2830 }, 2831 { 2832 name: "skipping unknown contexts - failing because c2 is failing", 2833 passing: false, 2834 combinedContexts: map[string]string{"c1": success, "c2": failure}, 2835 config: config.TideContextPolicy{ 2836 RequiredContexts: []string{"c1", "c2"}, 2837 OptionalContexts: []string{"c4"}, 2838 SkipUnknownContexts: &yes, 2839 }, 2840 availableContexts: []string{"c1", "c2"}, 2841 failedContexts: []string{"c2"}, 2842 }, 2843 { 2844 name: "skipping unknown contexts - passing because c4 is optional", 2845 passing: true, 2846 combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure}, 2847 config: config.TideContextPolicy{ 2848 RequiredContexts: []string{"c1", "c3"}, 2849 OptionalContexts: []string{"c4"}, 2850 SkipUnknownContexts: &yes, 2851 }, 2852 availableContexts: []string{"c1", "c2", "c3", "c4"}, 2853 }, 2854 { 2855 name: "skipping unknown contexts - passing because c4 is optional and c5 is unknown", 2856 passing: true, 2857 2858 combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure, "c5": failure}, 2859 config: config.TideContextPolicy{ 2860 RequiredContexts: []string{"c1", "c3"}, 2861 OptionalContexts: []string{"c4"}, 2862 SkipUnknownContexts: &yes, 2863 }, 2864 availableContexts: []string{"c1", "c2", "c3", "c4", "c5"}, 2865 }, 2866 } 2867 2868 for _, tc := range testCases { 2869 t.Run(tc.name, func(t *testing.T) { 2870 2871 ghc := &fgc{ 2872 combinedStatus: tc.combinedContexts, 2873 expectedSHA: headSHA} 2874 log := logrus.WithField("component", "tide") 2875 hook := test.NewGlobal() 2876 _, err := log.String() 2877 if err != nil { 2878 t.Fatalf("Failed to get log output before testing: %v", err) 2879 } 2880 syncCtl := &syncController{provider: &GitHubProvider{ghc: ghc, logger: log}} 2881 pr := PullRequest{HeadRefOID: githubql.String(headSHA)} 2882 passing := syncCtl.isPassingTests(log, CodeReviewCommonFromPullRequest(&pr), &tc.config) 2883 if passing != tc.passing { 2884 t.Errorf("%s: Expected %t got %t", tc.name, tc.passing, passing) 2885 } 2886 2887 // The last entry is used as the hook captures 2 different logs. 2888 // The required fields are available in the last entry and are validated. 2889 logFields := hook.LastEntry().Data 2890 assert.Equal(t, logFields["context_names"], tc.availableContexts) 2891 assert.Equal(t, logFields["failed_context_names"], tc.failedContexts) 2892 assert.Equal(t, logFields["total_context_count"], len(tc.availableContexts)) 2893 assert.Equal(t, logFields["failed_context_count"], len(tc.failedContexts)) 2894 if tc.passing { 2895 c := &syncController{ 2896 provider: &GitHubProvider{ghc: ghc, logger: log}, 2897 prowJobClient: fakectrlruntimeclient.NewClientBuilder().Build(), 2898 config: func() *config.Config { return &config.Config{} }, 2899 } 2900 // isRetestEligible is more lenient than isPassingTests, which means we expect it to allow 2901 // everything that is allowed by isPassingTests. The reverse might not be true. 2902 if !c.isRetestEligible(log, CodeReviewCommonFromPullRequest(&pr), &tc.config) { 2903 t.Error("expected pr to be batch testing eligible, wasn't the case") 2904 } 2905 } 2906 }) 2907 } 2908 } 2909 2910 func TestPresubmitsByPull(t *testing.T) { 2911 samplePR := PullRequest{ 2912 Number: githubql.Int(100), 2913 HeadRefOID: githubql.String("sha"), 2914 } 2915 testcases := []struct { 2916 name string 2917 2918 initialChangeCache map[changeCacheKey][]string 2919 presubmits []config.Presubmit 2920 prs []CodeReviewCommon 2921 prowYAMLGetter config.ProwYAMLGetter 2922 2923 expectedPresubmits map[int][]config.Presubmit 2924 expectedChangeCache map[changeCacheKey][]string 2925 requireManuallyTriggeredJobs bool 2926 fromBranchProtection bool 2927 }{ 2928 { 2929 name: "no matching presubmits", 2930 presubmits: []config.Presubmit{ 2931 { 2932 Reporter: config.Reporter{Context: "always"}, 2933 RegexpChangeMatcher: config.RegexpChangeMatcher{ 2934 RunIfChanged: "foo", 2935 }, 2936 }, 2937 { 2938 Reporter: config.Reporter{Context: "never"}, 2939 }, 2940 }, 2941 expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"CHANGED"}}, 2942 expectedPresubmits: map[int][]config.Presubmit{}, 2943 }, 2944 { 2945 name: "no presubmits", 2946 presubmits: []config.Presubmit{}, 2947 expectedPresubmits: map[int][]config.Presubmit{}, 2948 }, 2949 { 2950 name: "no matching presubmits (check cache eviction)", 2951 presubmits: []config.Presubmit{ 2952 { 2953 Reporter: config.Reporter{Context: "never"}, 2954 }, 2955 }, 2956 initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 2957 expectedPresubmits: map[int][]config.Presubmit{}, 2958 }, 2959 { 2960 name: "no matching presubmits (check cache retention)", 2961 presubmits: []config.Presubmit{ 2962 { 2963 Reporter: config.Reporter{Context: "always"}, 2964 RegexpChangeMatcher: config.RegexpChangeMatcher{ 2965 RunIfChanged: "foo", 2966 }, 2967 }, 2968 { 2969 Reporter: config.Reporter{Context: "never"}, 2970 }, 2971 }, 2972 initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 2973 expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 2974 expectedPresubmits: map[int][]config.Presubmit{}, 2975 }, 2976 { 2977 name: "always_run", 2978 presubmits: []config.Presubmit{ 2979 { 2980 Reporter: config.Reporter{Context: "always"}, 2981 AlwaysRun: true, 2982 }, 2983 { 2984 Reporter: config.Reporter{Context: "never"}, 2985 }, 2986 }, 2987 expectedPresubmits: map[int][]config.Presubmit{100: {{ 2988 Reporter: config.Reporter{Context: "always"}, 2989 AlwaysRun: true, 2990 }}}, 2991 }, 2992 { 2993 name: "runs against branch", 2994 presubmits: []config.Presubmit{ 2995 { 2996 Reporter: config.Reporter{Context: "presubmit"}, 2997 AlwaysRun: true, 2998 Brancher: config.Brancher{ 2999 Branches: []string{defaultBranch, "dev"}, 3000 }, 3001 }, 3002 { 3003 Reporter: config.Reporter{Context: "never"}, 3004 }, 3005 }, 3006 expectedPresubmits: map[int][]config.Presubmit{100: {{ 3007 Reporter: config.Reporter{Context: "presubmit"}, 3008 AlwaysRun: true, 3009 Brancher: config.Brancher{ 3010 Branches: []string{defaultBranch, "dev"}, 3011 }, 3012 }}}, 3013 }, 3014 { 3015 name: "doesn't run against branch", 3016 presubmits: []config.Presubmit{ 3017 { 3018 Reporter: config.Reporter{Context: "presubmit"}, 3019 AlwaysRun: true, 3020 Brancher: config.Brancher{ 3021 Branches: []string{"release", "dev"}, 3022 }, 3023 }, 3024 { 3025 Reporter: config.Reporter{Context: "always"}, 3026 AlwaysRun: true, 3027 }, 3028 { 3029 Reporter: config.Reporter{Context: "never"}, 3030 }, 3031 }, 3032 expectedPresubmits: map[int][]config.Presubmit{100: {{ 3033 Reporter: config.Reporter{Context: "always"}, 3034 AlwaysRun: true, 3035 }}}, 3036 }, 3037 { 3038 name: "no-always-run-no-trigger", 3039 presubmits: []config.Presubmit{ 3040 { 3041 Reporter: config.Reporter{Context: "presubmit"}, 3042 AlwaysRun: false, 3043 Brancher: config.Brancher{ 3044 Branches: []string{defaultBranch, "dev"}, 3045 }, 3046 }, 3047 { 3048 Reporter: config.Reporter{Context: "never"}, 3049 }, 3050 }, 3051 expectedPresubmits: map[int][]config.Presubmit{}, 3052 }, 3053 { 3054 name: "no-always-run-no-trigger-tide-wants-it", 3055 presubmits: []config.Presubmit{ 3056 { 3057 Reporter: config.Reporter{Context: "presubmit"}, 3058 AlwaysRun: false, 3059 Brancher: config.Brancher{ 3060 Branches: []string{defaultBranch, "dev"}, 3061 }, 3062 RunBeforeMerge: true, 3063 }, 3064 { 3065 Reporter: config.Reporter{Context: "never"}, 3066 }, 3067 }, 3068 expectedPresubmits: map[int][]config.Presubmit{100: {{ 3069 Reporter: config.Reporter{Context: "presubmit"}, 3070 AlwaysRun: false, 3071 RunBeforeMerge: true, 3072 Brancher: config.Brancher{ 3073 Branches: []string{defaultBranch, "dev"}, 3074 }, 3075 }}}, 3076 }, 3077 { 3078 name: "runs manual triggered jobs (requireManuallyTriggeredJobs enabled)", 3079 presubmits: []config.Presubmit{ 3080 { 3081 Reporter: config.Reporter{Context: "presubmit"}, 3082 AlwaysRun: false, 3083 Brancher: config.Brancher{ 3084 Branches: []string{defaultBranch, "dev"}, 3085 }, 3086 }, 3087 }, 3088 requireManuallyTriggeredJobs: true, 3089 fromBranchProtection: true, 3090 expectedPresubmits: map[int][]config.Presubmit{100: {{ 3091 Reporter: config.Reporter{Context: "presubmit"}, 3092 AlwaysRun: false, 3093 Brancher: config.Brancher{ 3094 Branches: []string{defaultBranch, "dev"}, 3095 }, 3096 }}}, 3097 }, 3098 { 3099 name: "doesn't run manual triggered jobs (requireManuallyTriggeredJobs disabled)", 3100 presubmits: []config.Presubmit{ 3101 { 3102 Reporter: config.Reporter{Context: "presubmit"}, 3103 AlwaysRun: false, 3104 Brancher: config.Brancher{ 3105 Branches: []string{defaultBranch, "dev"}, 3106 }, 3107 }, 3108 }, 3109 fromBranchProtection: true, 3110 expectedPresubmits: map[int][]config.Presubmit{}, 3111 }, 3112 { 3113 name: "brancher-not-match-when-tide-wants-it", 3114 presubmits: []config.Presubmit{ 3115 { 3116 Reporter: config.Reporter{Context: "presubmit"}, 3117 AlwaysRun: false, 3118 Brancher: config.Brancher{ 3119 Branches: []string{"release", "dev"}, 3120 }, 3121 RunBeforeMerge: true, 3122 }, 3123 { 3124 Reporter: config.Reporter{Context: "never"}, 3125 }, 3126 }, 3127 expectedPresubmits: map[int][]config.Presubmit{}, 3128 }, 3129 { 3130 name: "run_if_changed (uncached)", 3131 presubmits: []config.Presubmit{ 3132 { 3133 Reporter: config.Reporter{Context: "presubmit"}, 3134 RegexpChangeMatcher: config.RegexpChangeMatcher{ 3135 RunIfChanged: "^CHANGE.$", 3136 }, 3137 }, 3138 { 3139 Reporter: config.Reporter{Context: "always"}, 3140 AlwaysRun: true, 3141 }, 3142 { 3143 Reporter: config.Reporter{Context: "never"}, 3144 }, 3145 }, 3146 expectedPresubmits: map[int][]config.Presubmit{100: {{ 3147 Reporter: config.Reporter{Context: "presubmit"}, 3148 RegexpChangeMatcher: config.RegexpChangeMatcher{ 3149 RunIfChanged: "^CHANGE.$", 3150 }, 3151 }, { 3152 Reporter: config.Reporter{Context: "always"}, 3153 AlwaysRun: true, 3154 }}}, 3155 expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"CHANGED"}}, 3156 }, 3157 { 3158 name: "run_if_changed (cached)", 3159 presubmits: []config.Presubmit{ 3160 { 3161 Reporter: config.Reporter{Context: "presubmit"}, 3162 RegexpChangeMatcher: config.RegexpChangeMatcher{ 3163 RunIfChanged: "^FIL.$", 3164 }, 3165 }, 3166 { 3167 Reporter: config.Reporter{Context: "always"}, 3168 AlwaysRun: true, 3169 }, 3170 { 3171 Reporter: config.Reporter{Context: "never"}, 3172 }, 3173 }, 3174 initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 3175 expectedPresubmits: map[int][]config.Presubmit{100: {{ 3176 Reporter: config.Reporter{Context: "presubmit"}, 3177 RegexpChangeMatcher: config.RegexpChangeMatcher{ 3178 RunIfChanged: "^FIL.$", 3179 }, 3180 }, 3181 { 3182 Reporter: config.Reporter{Context: "always"}, 3183 AlwaysRun: true, 3184 }}}, 3185 expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 3186 }, 3187 { 3188 name: "run_if_changed (cached) (skippable)", 3189 presubmits: []config.Presubmit{ 3190 { 3191 Reporter: config.Reporter{Context: "presubmit"}, 3192 RegexpChangeMatcher: config.RegexpChangeMatcher{ 3193 RunIfChanged: "^CHANGE.$", 3194 }, 3195 }, 3196 { 3197 Reporter: config.Reporter{Context: "always"}, 3198 AlwaysRun: true, 3199 }, 3200 { 3201 Reporter: config.Reporter{Context: "never"}, 3202 }, 3203 }, 3204 initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 3205 expectedPresubmits: map[int][]config.Presubmit{100: {{ 3206 Reporter: config.Reporter{Context: "always"}, 3207 AlwaysRun: true, 3208 }}}, 3209 expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 3210 }, 3211 { 3212 name: "inrepoconfig presubmits get only added to the corresponding pull", 3213 presubmits: []config.Presubmit{{ 3214 AlwaysRun: true, 3215 Reporter: config.Reporter{Context: "always"}, 3216 }}, 3217 prowYAMLGetter: prowYAMLGetterForHeadRefs([]string{"1"}, []config.Presubmit{{ 3218 AlwaysRun: true, 3219 Reporter: config.Reporter{Context: "inrepoconfig"}, 3220 }}), 3221 prs: []CodeReviewCommon{ 3222 {Number: 1, HeadRefOID: "1"}, 3223 }, 3224 expectedPresubmits: map[int][]config.Presubmit{ 3225 1: { 3226 {AlwaysRun: true, Reporter: config.Reporter{Context: "always"}}, 3227 {AlwaysRun: true, Reporter: config.Reporter{Context: "inrepoconfig"}}, 3228 }, 3229 100: { 3230 {AlwaysRun: true, Reporter: config.Reporter{Context: "always"}}, 3231 }, 3232 }, 3233 }, 3234 { 3235 name: "broken inrepoconfig doesn't break the whole subpool", 3236 presubmits: []config.Presubmit{{ 3237 AlwaysRun: true, 3238 Reporter: config.Reporter{Context: "always"}, 3239 }}, 3240 prowYAMLGetter: func(_ *config.Config, _ git.ClientFactory, _, _, _ string, headRefs ...string) (*config.ProwYAML, error) { 3241 if len(headRefs) == 1 && headRefs[0] == "1" { 3242 return nil, errors.New("you shall not get jobs") 3243 } 3244 return &config.ProwYAML{}, nil 3245 }, 3246 prs: []CodeReviewCommon{ 3247 {Number: 1, HeadRefOID: "1"}, 3248 }, 3249 expectedPresubmits: map[int][]config.Presubmit{ 3250 100: { 3251 {AlwaysRun: true, Reporter: config.Reporter{Context: "always"}}, 3252 }, 3253 }, 3254 }, 3255 } 3256 3257 for _, tc := range testcases { 3258 tc := tc 3259 t.Run(tc.name, func(t *testing.T) { 3260 if tc.initialChangeCache == nil { 3261 tc.initialChangeCache = map[changeCacheKey][]string{} 3262 } 3263 if tc.expectedChangeCache == nil { 3264 tc.expectedChangeCache = map[changeCacheKey][]string{} 3265 } 3266 3267 cfg := &config.Config{ 3268 ProwConfig: config.ProwConfig{ 3269 BranchProtection: config.BranchProtection{ 3270 Policy: config.Policy{ 3271 RequireManuallyTriggeredJobs: &tc.requireManuallyTriggeredJobs, 3272 }, 3273 }, 3274 Tide: config.Tide{ 3275 TideGitHubConfig: config.TideGitHubConfig{ 3276 ContextOptions: config.TideContextPolicyOptions{ 3277 TideContextPolicy: config.TideContextPolicy{ 3278 FromBranchProtection: &tc.fromBranchProtection, 3279 }, 3280 }, 3281 }, 3282 }, 3283 }, 3284 } 3285 3286 cfg.SetPresubmits(map[string][]config.Presubmit{ 3287 "/": tc.presubmits, 3288 "foo/bar": {{Reporter: config.Reporter{Context: "wrong-repo"}, AlwaysRun: true}}, 3289 }) 3290 if tc.prowYAMLGetter != nil { 3291 cfg.InRepoConfig.Enabled = map[string]*bool{"*": utilpointer.Bool(true)} 3292 cfg.ProwYAMLGetterWithDefaults = tc.prowYAMLGetter 3293 } 3294 cfgAgent := &config.Agent{} 3295 cfgAgent.Set(cfg) 3296 sp := &subpool{ 3297 branch: defaultBranch, 3298 sha: "master-sha", 3299 prs: append(tc.prs, *CodeReviewCommonFromPullRequest(&samplePR)), 3300 } 3301 log := logrus.WithField("test", tc.name) 3302 ghProvider := newGitHubProvider(log, &fgc{}, nil, cfgAgent.Config, newMergeChecker(cfgAgent.Config, &fgc{}), false) 3303 c := &syncController{ 3304 config: cfgAgent.Config, 3305 provider: ghProvider, 3306 changedFiles: &changedFilesAgent{ 3307 provider: ghProvider, 3308 changeCache: tc.initialChangeCache, 3309 nextChangeCache: make(map[changeCacheKey][]string), 3310 }, 3311 logger: log, 3312 } 3313 presubmits, err := c.presubmitsByPull(sp) 3314 if err != nil { 3315 t.Fatalf("unexpected error from presubmitsByPull: %v", err) 3316 } 3317 c.changedFiles.prune() 3318 // for equality we need to clear the compiled regexes 3319 for _, jobs := range presubmits { 3320 config.ClearCompiledRegexes(jobs) 3321 } 3322 if !apiequality.Semantic.DeepEqual(presubmits, tc.expectedPresubmits) { 3323 t.Errorf("got incorrect presubmit mapping: %v\n", diff.ObjectReflectDiff(tc.expectedPresubmits, presubmits)) 3324 } 3325 if got := c.changedFiles.changeCache; !reflect.DeepEqual(got, tc.expectedChangeCache) { 3326 t.Errorf("got incorrect file change cache: %v", diff.ObjectReflectDiff(tc.expectedChangeCache, got)) 3327 } 3328 }) 3329 } 3330 } 3331 3332 func getTemplate(name, tplStr string) *template.Template { 3333 tpl, _ := template.New(name).Parse(tplStr) 3334 return tpl 3335 } 3336 3337 func TestAccumulateReturnsCorrectMissingTests(t *testing.T) { 3338 const baseSHA = "8d287a3aeae90fd0aef4a70009c715712ff302cd" 3339 3340 testCases := []struct { 3341 name string 3342 presubmits map[int][]config.Presubmit 3343 prs []PullRequest 3344 pjs []prowapi.ProwJob 3345 expectedPresubmits map[int][]config.Presubmit 3346 }{ 3347 { 3348 name: "All presubmits missing, no changes", 3349 prs: []PullRequest{{ 3350 Number: 1, 3351 HeadRefOID: "sha", 3352 }}, 3353 presubmits: map[int][]config.Presubmit{1: {{ 3354 Reporter: config.Reporter{ 3355 Context: "my-presubmit", 3356 }, 3357 }}}, 3358 expectedPresubmits: map[int][]config.Presubmit{ 3359 1: {{Reporter: config.Reporter{Context: "my-presubmit"}}}, 3360 }, 3361 }, 3362 { 3363 name: "All presubmits successful, no retesting needed", 3364 prs: []PullRequest{{ 3365 Number: 1, 3366 HeadRefOID: "sha", 3367 }}, 3368 pjs: []prowapi.ProwJob{{ 3369 Spec: prowapi.ProwJobSpec{ 3370 Type: prowapi.PresubmitJob, 3371 Refs: &prowapi.Refs{ 3372 Pulls: []prowapi.Pull{{ 3373 Number: 1, 3374 SHA: "sha", 3375 }}, 3376 }, 3377 Context: "my-presubmit", 3378 }, 3379 Status: prowapi.ProwJobStatus{State: prowapi.SuccessState}, 3380 }}, 3381 presubmits: map[int][]config.Presubmit{ 3382 1: {{Reporter: config.Reporter{Context: "my-presubmit"}}}, 3383 }, 3384 }, 3385 { 3386 name: "All presubmits pending, no retesting needed", 3387 prs: []PullRequest{{ 3388 Number: 1, 3389 HeadRefOID: "sha", 3390 }}, 3391 pjs: []prowapi.ProwJob{{ 3392 Spec: prowapi.ProwJobSpec{ 3393 Type: prowapi.PresubmitJob, 3394 Refs: &prowapi.Refs{ 3395 Pulls: []prowapi.Pull{{ 3396 Number: 1, 3397 SHA: "sha", 3398 }}, 3399 }, 3400 Context: "my-presubmit", 3401 }, 3402 Status: prowapi.ProwJobStatus{State: prowapi.PendingState}, 3403 }}, 3404 presubmits: map[int][]config.Presubmit{ 3405 1: {{Reporter: config.Reporter{Context: "my-presubmit"}}}}, 3406 }, 3407 { 3408 name: "One successful, one pending, one missing, one failing, only missing and failing remain", 3409 prs: []PullRequest{{ 3410 Number: 1, 3411 HeadRefOID: "sha", 3412 }}, 3413 pjs: []prowapi.ProwJob{ 3414 { 3415 Spec: prowapi.ProwJobSpec{ 3416 Type: prowapi.PresubmitJob, 3417 Refs: &prowapi.Refs{ 3418 Pulls: []prowapi.Pull{{ 3419 Number: 1, 3420 SHA: "sha", 3421 }}, 3422 }, 3423 Context: "my-successful-presubmit", 3424 }, 3425 Status: prowapi.ProwJobStatus{State: prowapi.SuccessState}, 3426 }, 3427 { 3428 Spec: prowapi.ProwJobSpec{ 3429 Type: prowapi.PresubmitJob, 3430 Refs: &prowapi.Refs{ 3431 Pulls: []prowapi.Pull{{ 3432 Number: 1, 3433 SHA: "sha", 3434 }}, 3435 }, 3436 Context: "my-pending-presubmit", 3437 }, 3438 Status: prowapi.ProwJobStatus{State: prowapi.PendingState}, 3439 }, 3440 { 3441 Spec: prowapi.ProwJobSpec{ 3442 Type: prowapi.PresubmitJob, 3443 Refs: &prowapi.Refs{ 3444 Pulls: []prowapi.Pull{{ 3445 Number: 1, 3446 SHA: "sha", 3447 }}, 3448 }, 3449 Context: "my-failing-presubmit", 3450 }, 3451 Status: prowapi.ProwJobStatus{State: prowapi.FailureState}, 3452 }, 3453 }, 3454 presubmits: map[int][]config.Presubmit{ 3455 1: { 3456 {Reporter: config.Reporter{Context: "my-successful-presubmit"}}, 3457 {Reporter: config.Reporter{Context: "my-pending-presubmit"}}, 3458 {Reporter: config.Reporter{Context: "my-failing-presubmit"}}, 3459 {Reporter: config.Reporter{Context: "my-missing-presubmit"}}, 3460 }}, 3461 expectedPresubmits: map[int][]config.Presubmit{ 3462 1: { 3463 {Reporter: config.Reporter{Context: "my-failing-presubmit"}}, 3464 {Reporter: config.Reporter{Context: "my-missing-presubmit"}}, 3465 }}, 3466 }, 3467 { 3468 name: "Two prs, each with one successful, one pending, one missing, one failing, only missing and failing remain", 3469 prs: []PullRequest{ 3470 { 3471 Number: 1, 3472 HeadRefOID: "sha", 3473 }, 3474 { 3475 Number: 2, 3476 HeadRefOID: "sha", 3477 }, 3478 }, 3479 pjs: []prowapi.ProwJob{ 3480 { 3481 Spec: prowapi.ProwJobSpec{ 3482 Type: prowapi.PresubmitJob, 3483 Refs: &prowapi.Refs{ 3484 Pulls: []prowapi.Pull{{ 3485 Number: 1, 3486 SHA: "sha", 3487 }}, 3488 }, 3489 Context: "my-successful-presubmit", 3490 }, 3491 Status: prowapi.ProwJobStatus{State: prowapi.SuccessState}, 3492 }, 3493 { 3494 Spec: prowapi.ProwJobSpec{ 3495 Type: prowapi.PresubmitJob, 3496 Refs: &prowapi.Refs{ 3497 Pulls: []prowapi.Pull{{ 3498 Number: 1, 3499 SHA: "sha", 3500 }}, 3501 }, 3502 Context: "my-pending-presubmit", 3503 }, 3504 Status: prowapi.ProwJobStatus{State: prowapi.PendingState}, 3505 }, 3506 { 3507 Spec: prowapi.ProwJobSpec{ 3508 Type: prowapi.PresubmitJob, 3509 Refs: &prowapi.Refs{ 3510 Pulls: []prowapi.Pull{{ 3511 Number: 1, 3512 SHA: "sha", 3513 }}, 3514 }, 3515 Context: "my-failing-presubmit", 3516 }, 3517 Status: prowapi.ProwJobStatus{State: prowapi.FailureState}, 3518 }, 3519 { 3520 Spec: prowapi.ProwJobSpec{ 3521 Type: prowapi.PresubmitJob, 3522 Refs: &prowapi.Refs{ 3523 Pulls: []prowapi.Pull{{ 3524 Number: 2, 3525 SHA: "sha", 3526 }}, 3527 }, 3528 Context: "my-successful-presubmit", 3529 }, 3530 Status: prowapi.ProwJobStatus{State: prowapi.SuccessState}, 3531 }, 3532 { 3533 Spec: prowapi.ProwJobSpec{ 3534 Type: prowapi.PresubmitJob, 3535 Refs: &prowapi.Refs{ 3536 Pulls: []prowapi.Pull{{ 3537 Number: 2, 3538 SHA: "sha", 3539 }}, 3540 }, 3541 Context: "my-pending-presubmit", 3542 }, 3543 Status: prowapi.ProwJobStatus{State: prowapi.PendingState}, 3544 }, 3545 { 3546 Spec: prowapi.ProwJobSpec{ 3547 Type: prowapi.PresubmitJob, 3548 Refs: &prowapi.Refs{ 3549 Pulls: []prowapi.Pull{{ 3550 Number: 2, 3551 SHA: "sha", 3552 }}, 3553 }, 3554 Context: "my-failing-presubmit", 3555 }, 3556 Status: prowapi.ProwJobStatus{State: prowapi.FailureState}, 3557 }, 3558 }, 3559 presubmits: map[int][]config.Presubmit{ 3560 1: { 3561 {Reporter: config.Reporter{Context: "my-successful-presubmit"}}, 3562 {Reporter: config.Reporter{Context: "my-pending-presubmit"}}, 3563 {Reporter: config.Reporter{Context: "my-failing-presubmit"}}, 3564 {Reporter: config.Reporter{Context: "my-missing-presubmit"}}, 3565 }, 3566 2: { 3567 {Reporter: config.Reporter{Context: "my-successful-presubmit"}}, 3568 {Reporter: config.Reporter{Context: "my-pending-presubmit"}}, 3569 {Reporter: config.Reporter{Context: "my-failing-presubmit"}}, 3570 {Reporter: config.Reporter{Context: "my-missing-presubmit"}}, 3571 }, 3572 }, 3573 expectedPresubmits: map[int][]config.Presubmit{ 3574 1: { 3575 {Reporter: config.Reporter{Context: "my-failing-presubmit"}}, 3576 {Reporter: config.Reporter{Context: "my-missing-presubmit"}}, 3577 }, 3578 2: { 3579 {Reporter: config.Reporter{Context: "my-failing-presubmit"}}, 3580 {Reporter: config.Reporter{Context: "my-missing-presubmit"}}, 3581 }, 3582 }, 3583 }, 3584 { 3585 name: "Result from successful context gets respected", 3586 presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job-1"}}}}, 3587 prs: []PullRequest{{ 3588 Number: 1, 3589 HeadRefOID: "headsha", 3590 Commits: Commits{Nodes: []struct{ Commit Commit }{{Commit: Commit{ 3591 OID: githubql.String("headsha"), 3592 Status: CommitStatus{Contexts: []Context{{ 3593 Context: githubql.String("job-1"), 3594 Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA), 3595 State: githubql.StatusStateSuccess, 3596 }}}, 3597 }}}}}}, 3598 }, 3599 { 3600 name: "Result from successful context gets respected with deprecated baseha delimiter", 3601 presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job-1"}}}}, 3602 prs: []PullRequest{{ 3603 Number: 1, 3604 HeadRefOID: "headsha", 3605 Commits: Commits{Nodes: []struct{ Commit Commit }{{Commit: Commit{ 3606 OID: githubql.String("headsha"), 3607 Status: CommitStatus{Contexts: []Context{{ 3608 Context: githubql.String("job-1"), 3609 Description: githubql.String("Job succeeded. Basesha:" + baseSHA), 3610 State: githubql.StatusStateSuccess, 3611 }}}, 3612 }}}}}}, 3613 }, 3614 { 3615 name: "Result from failed context gets ignored", 3616 presubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job-1"}}}}, 3617 prs: []PullRequest{{ 3618 Number: 1, 3619 HeadRefOID: "headsha", 3620 Commits: Commits{Nodes: []struct{ Commit Commit }{{Commit: Commit{ 3621 OID: githubql.String("headsha"), 3622 Status: CommitStatus{Contexts: []Context{{ 3623 Context: githubql.String("job-1"), 3624 Description: githubql.String("Job succeeded. BaseSHA:" + baseSHA), 3625 State: githubql.StatusStateFailure, 3626 }}}, 3627 }}}}}}, 3628 expectedPresubmits: map[int][]config.Presubmit{1: {{Reporter: config.Reporter{Context: "job-1"}}}}, 3629 }, 3630 } 3631 3632 log := logrus.NewEntry(logrus.New()) 3633 for _, tc := range testCases { 3634 t.Run(tc.name, func(t *testing.T) { 3635 var crcs []CodeReviewCommon 3636 for _, pr := range tc.prs { 3637 crc := CodeReviewCommonFromPullRequest(&pr) 3638 crcs = append(crcs, *crc) 3639 } 3640 syncCtrl := &syncController{ 3641 provider: &GitHubProvider{ 3642 ghc: &fgc{}, 3643 logger: log, 3644 }, 3645 logger: log, 3646 } 3647 _, _, _, missingSerialTests := syncCtrl.accumulate(tc.presubmits, crcs, tc.pjs, baseSHA) 3648 // Apiequality treats nil slices/maps equal to a zero length slice/map, keeping us from 3649 // the burden of having to always initialize them 3650 if !apiequality.Semantic.DeepEqual(tc.expectedPresubmits, missingSerialTests) { 3651 t.Errorf("expected \n%v\n to be \n%v\n", missingSerialTests, tc.expectedPresubmits) 3652 } 3653 }) 3654 } 3655 } 3656 3657 func TestPresubmitsForBatch(t *testing.T) { 3658 testCases := []struct { 3659 name string 3660 prs []CodeReviewCommon 3661 changedFiles *changedFilesAgent 3662 jobs []config.Presubmit 3663 prowYAMLGetter config.ProwYAMLGetter 3664 expected []config.Presubmit 3665 requireManuallyTriggeredJobs bool 3666 fromBranchProtection bool 3667 }{ 3668 { 3669 name: "All jobs get picked", 3670 prs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1))}, 3671 jobs: []config.Presubmit{{ 3672 AlwaysRun: true, 3673 Reporter: config.Reporter{Context: "foo"}, 3674 }}, 3675 expected: []config.Presubmit{{ 3676 AlwaysRun: true, 3677 Reporter: config.Reporter{Context: "foo"}, 3678 }}, 3679 }, 3680 { 3681 name: "Jobs with branchconfig get picked", 3682 prs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1))}, 3683 jobs: []config.Presubmit{{ 3684 AlwaysRun: true, 3685 Reporter: config.Reporter{Context: "foo"}, 3686 Brancher: config.Brancher{Branches: []string{defaultBranch}}, 3687 }}, 3688 expected: []config.Presubmit{{ 3689 AlwaysRun: true, 3690 Reporter: config.Reporter{Context: "foo"}, 3691 Brancher: config.Brancher{Branches: []string{defaultBranch}}, 3692 }}, 3693 }, 3694 { 3695 name: "Optional jobs are excluded", 3696 prs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1))}, 3697 jobs: []config.Presubmit{ 3698 { 3699 AlwaysRun: true, 3700 Reporter: config.Reporter{Context: "foo"}, 3701 }, 3702 { 3703 Reporter: config.Reporter{Context: "bar"}, 3704 }, 3705 }, 3706 expected: []config.Presubmit{{ 3707 AlwaysRun: true, 3708 Reporter: config.Reporter{Context: "foo"}, 3709 }}, 3710 }, 3711 { 3712 name: "jobs that require manual trigger included with branch protection enabled", 3713 prs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1))}, 3714 requireManuallyTriggeredJobs: true, 3715 fromBranchProtection: true, 3716 jobs: []config.Presubmit{ 3717 { 3718 AlwaysRun: true, 3719 Reporter: config.Reporter{Context: "foo"}, 3720 }, 3721 { 3722 Reporter: config.Reporter{Context: "bar"}, 3723 }, 3724 }, 3725 expected: []config.Presubmit{ 3726 { 3727 AlwaysRun: true, 3728 Reporter: config.Reporter{Context: "foo"}, 3729 }, 3730 { 3731 Reporter: config.Reporter{Context: "bar"}, 3732 }}, 3733 }, 3734 { 3735 name: "jobs that require manual trigger excluded with branch protection disabled", 3736 prs: []CodeReviewCommon{*CodeReviewCommonFromPullRequest(getPR("org", "repo", 1))}, 3737 jobs: []config.Presubmit{ 3738 { 3739 AlwaysRun: true, 3740 Reporter: config.Reporter{Context: "foo"}, 3741 }, 3742 { 3743 Reporter: config.Reporter{Context: "bar"}, 3744 }, 3745 }, 3746 expected: []config.Presubmit{{ 3747 AlwaysRun: true, 3748 Reporter: config.Reporter{Context: "foo"}, 3749 }}, 3750 }, 3751 { 3752 name: "Jobs that are required by any of the PRs get included", 3753 prs: []CodeReviewCommon{ 3754 *CodeReviewCommonFromPullRequest(getPR("org", "repo", 2)), 3755 *CodeReviewCommonFromPullRequest(getPR("org", "repo", 1, func(pr *PullRequest) { 3756 pr.HeadRefOID = githubql.String("sha") 3757 })), 3758 }, 3759 jobs: []config.Presubmit{{ 3760 RegexpChangeMatcher: config.RegexpChangeMatcher{ 3761 RunIfChanged: "/very-important", 3762 }, 3763 Reporter: config.Reporter{Context: "foo"}, 3764 }}, 3765 changedFiles: &changedFilesAgent{ 3766 changeCache: map[changeCacheKey][]string{ 3767 {org: "org", repo: "repo", number: 1, sha: "sha"}: {"/very-important"}, 3768 {org: "org", repo: "repo", number: 2}: {}, 3769 }, 3770 nextChangeCache: map[changeCacheKey][]string{}, 3771 }, 3772 expected: []config.Presubmit{{ 3773 RegexpChangeMatcher: config.RegexpChangeMatcher{ 3774 RunIfChanged: "/very-important", 3775 }, 3776 Reporter: config.Reporter{Context: "foo"}, 3777 }}, 3778 }, 3779 { 3780 name: "Inrepoconfig jobs get included if headref matches", 3781 prs: []CodeReviewCommon{ 3782 *CodeReviewCommonFromPullRequest(getPR("org", "repo", 2, func(pr *PullRequest) { 3783 pr.HeadRefOID = githubql.String("sha2") 3784 })), 3785 *CodeReviewCommonFromPullRequest(getPR("org", "repo", 1, func(pr *PullRequest) { 3786 pr.HeadRefOID = githubql.String("sha1") 3787 })), 3788 }, 3789 jobs: []config.Presubmit{ 3790 { 3791 AlwaysRun: true, 3792 Reporter: config.Reporter{Context: "foo"}, 3793 }, 3794 }, 3795 prowYAMLGetter: prowYAMLGetterForHeadRefs([]string{"sha1", "sha2"}, []config.Presubmit{{ 3796 AlwaysRun: true, 3797 Reporter: config.Reporter{Context: "bar"}, 3798 }}), 3799 expected: []config.Presubmit{ 3800 { 3801 AlwaysRun: true, 3802 Reporter: config.Reporter{Context: "foo"}, 3803 }, 3804 { 3805 AlwaysRun: true, 3806 Reporter: config.Reporter{Context: "bar"}, 3807 }, 3808 }, 3809 }, 3810 { 3811 name: "Inrepoconfig jobs do not get included if headref doesnt match", 3812 prs: []CodeReviewCommon{ 3813 *CodeReviewCommonFromPullRequest(getPR("org", "repo", 2, func(pr *PullRequest) { 3814 pr.HeadRefOID = githubql.String("sha2") 3815 })), 3816 *CodeReviewCommonFromPullRequest(getPR("org", "repo", 1, func(pr *PullRequest) { 3817 pr.HeadRefOID = githubql.String("sha1") 3818 })), 3819 }, 3820 jobs: []config.Presubmit{ 3821 { 3822 AlwaysRun: true, 3823 Reporter: config.Reporter{Context: "foo"}, 3824 }, 3825 }, 3826 prowYAMLGetter: prowYAMLGetterForHeadRefs([]string{"other-sha", "sha2"}, []config.Presubmit{{ 3827 AlwaysRun: true, 3828 Reporter: config.Reporter{Context: "bar"}, 3829 }}), 3830 expected: []config.Presubmit{ 3831 { 3832 AlwaysRun: true, 3833 Reporter: config.Reporter{Context: "foo"}, 3834 }, 3835 }, 3836 }, 3837 } 3838 3839 for _, tc := range testCases { 3840 t.Run(tc.name, func(t *testing.T) { 3841 3842 if tc.changedFiles == nil { 3843 tc.changedFiles = &changedFilesAgent{ 3844 changeCache: map[changeCacheKey][]string{}, 3845 } 3846 for _, pr := range tc.prs { 3847 key := changeCacheKey{ 3848 org: pr.Org, 3849 repo: pr.Repo, 3850 number: int(pr.Number), 3851 sha: string(pr.HeadRefOID), 3852 } 3853 tc.changedFiles.changeCache[key] = []string{} 3854 } 3855 } 3856 3857 if err := config.SetPresubmitRegexes(tc.jobs); err != nil { 3858 t.Fatalf("failed to set presubmit regexes: %v", err) 3859 } 3860 3861 inrepoconfig := config.InRepoConfig{} 3862 if tc.prowYAMLGetter != nil { 3863 inrepoconfig.Enabled = map[string]*bool{"*": utilpointer.Bool(true)} 3864 } 3865 cfg := func() *config.Config { 3866 return &config.Config{ 3867 JobConfig: config.JobConfig{ 3868 PresubmitsStatic: map[string][]config.Presubmit{ 3869 "org/repo": tc.jobs, 3870 }, 3871 ProwYAMLGetterWithDefaults: tc.prowYAMLGetter, 3872 }, 3873 ProwConfig: config.ProwConfig{ 3874 InRepoConfig: inrepoconfig, 3875 BranchProtection: config.BranchProtection{ 3876 Orgs: map[string]config.Org{ 3877 "org": { 3878 Policy: config.Policy{ 3879 RequireManuallyTriggeredJobs: &tc.requireManuallyTriggeredJobs, 3880 }, 3881 }, 3882 }, 3883 }, 3884 Tide: config.Tide{ 3885 TideGitHubConfig: config.TideGitHubConfig{ 3886 ContextOptions: config.TideContextPolicyOptions{ 3887 TideContextPolicy: config.TideContextPolicy{ 3888 FromBranchProtection: &tc.fromBranchProtection, 3889 }, 3890 }, 3891 }, 3892 }, 3893 }, 3894 } 3895 } 3896 c := &syncController{ 3897 provider: newGitHubProvider(logrus.WithField("test", tc.name), nil, nil, cfg, nil, false), 3898 changedFiles: tc.changedFiles, 3899 config: cfg, 3900 logger: logrus.WithField("test", tc.name), 3901 } 3902 3903 presubmits, err := c.presubmitsForBatch(tc.prs, "org", "repo", "baseSHA", defaultBranch) 3904 if err != nil { 3905 t.Fatalf("failed to get presubmits for batch: %v", err) 3906 } 3907 // Clear regexes, otherwise DeepEqual comparison wont work 3908 config.ClearCompiledRegexes(presubmits) 3909 if !apiequality.Semantic.DeepEqual(tc.expected, presubmits) { 3910 t.Errorf("returned presubmits do not match expected, diff: %v\n", diff.ObjectReflectDiff(tc.expected, presubmits)) 3911 } 3912 }) 3913 } 3914 } 3915 3916 func TestChangedFilesAgentBatchChanges(t *testing.T) { 3917 testCases := []struct { 3918 name string 3919 prs []CodeReviewCommon 3920 changedFiles *changedFilesAgent 3921 expected []string 3922 }{ 3923 { 3924 name: "Single PR", 3925 prs: []CodeReviewCommon{ 3926 *CodeReviewCommonFromPullRequest(getPR("org", "repo", 1)), 3927 }, 3928 changedFiles: &changedFilesAgent{ 3929 changeCache: map[changeCacheKey][]string{ 3930 {org: "org", repo: "repo", number: 1}: {"foo"}, 3931 }, 3932 }, 3933 expected: []string{"foo"}, 3934 }, 3935 { 3936 name: "Multiple PRs, changes are de-duplicated", 3937 prs: []CodeReviewCommon{ 3938 *CodeReviewCommonFromPullRequest(getPR("org", "repo", 1)), 3939 *CodeReviewCommonFromPullRequest(getPR("org", "repo", 2)), 3940 }, 3941 changedFiles: &changedFilesAgent{ 3942 changeCache: map[changeCacheKey][]string{ 3943 {org: "org", repo: "repo", number: 1}: {"foo"}, 3944 {org: "org", repo: "repo", number: 2}: {"foo", "bar"}, 3945 }, 3946 }, 3947 expected: []string{"bar", "foo"}, 3948 }, 3949 } 3950 3951 for _, tc := range testCases { 3952 t.Run(tc.name, func(t *testing.T) { 3953 tc.changedFiles.nextChangeCache = map[changeCacheKey][]string{} 3954 3955 result, err := tc.changedFiles.batchChanges(tc.prs)() 3956 if err != nil { 3957 t.Fatalf("fauked to get changed files: %v", err) 3958 } 3959 if !apiequality.Semantic.DeepEqual(result, tc.expected) { 3960 t.Errorf("returned changes do not match expected; diff: %v\n", diff.ObjectReflectDiff(tc.expected, result)) 3961 } 3962 }) 3963 } 3964 } 3965 3966 func getPR(org, name string, number int, opts ...func(*PullRequest)) *PullRequest { 3967 pr := PullRequest{} 3968 pr.Repository.Owner.Login = githubql.String(org) 3969 pr.Repository.NameWithOwner = githubql.String(org + "/" + name) 3970 pr.Repository.Name = githubql.String(name) 3971 pr.Number = githubql.Int(number) 3972 for _, opt := range opts { 3973 opt(&pr) 3974 } 3975 return &pr 3976 } 3977 3978 func TestCacheIndexFuncReturnsDifferentResultsForDifferentInputs(t *testing.T) { 3979 type orgRepoBranch struct{ org, repo, branch string } 3980 3981 results := sets.Set[string]{} 3982 inputs := []orgRepoBranch{ 3983 {"org-a", "repo-a", "branch-a"}, 3984 {"org-a", "repo-a", "branch-b"}, 3985 {"org-a", "repo-b", "branch-a"}, 3986 {"org-b", "repo-a", "branch-a"}, 3987 } 3988 for _, input := range inputs { 3989 pj := getProwJob(prowapi.PresubmitJob, input.org, input.repo, input.branch, "123", "", nil) 3990 idx := cacheIndexFunc(pj) 3991 if n := len(idx); n != 1 { 3992 t.Fatalf("expected to get exactly one index back, got %d", n) 3993 } 3994 if results.Has(idx[0]) { 3995 t.Errorf("got duplicate idx %q", idx) 3996 } 3997 results.Insert(idx[0]) 3998 } 3999 } 4000 4001 func TestCacheIndexFunc(t *testing.T) { 4002 testCases := []struct { 4003 name string 4004 prowjob *prowapi.ProwJob 4005 expectedResult string 4006 }{ 4007 { 4008 name: "Wrong type, no result", 4009 prowjob: &prowapi.ProwJob{}, 4010 }, 4011 { 4012 name: "No refs, no result", 4013 prowjob: getProwJob(prowapi.PresubmitJob, "", "", "", "", "", nil), 4014 }, 4015 { 4016 name: "presubmit job", 4017 prowjob: getProwJob(prowapi.PresubmitJob, "org", "repo", "master", "123", "", nil), 4018 expectedResult: "org/repo:master@123", 4019 }, 4020 { 4021 name: "Batch job", 4022 prowjob: getProwJob(prowapi.BatchJob, "org", "repo", "next", "1234", "", nil), 4023 expectedResult: "org/repo:next@1234", 4024 }, 4025 } 4026 4027 for idx := range testCases { 4028 tc := testCases[idx] 4029 t.Run(tc.name, func(t *testing.T) { 4030 result := cacheIndexFunc(tc.prowjob) 4031 if n := len(result); n > 1 { 4032 t.Errorf("expected at most one result, got %d", n) 4033 } 4034 4035 var resultString string 4036 if len(result) == 1 { 4037 resultString = result[0] 4038 } 4039 4040 if resultString != tc.expectedResult { 4041 t.Errorf("Expected result %q, got result %q", tc.expectedResult, resultString) 4042 } 4043 }) 4044 } 4045 } 4046 4047 func getProwJob(pjtype prowapi.ProwJobType, org, repo, branch, sha string, state prowapi.ProwJobState, pulls []prowapi.Pull) *prowapi.ProwJob { 4048 pj := &prowapi.ProwJob{} 4049 pj.Spec.Type = pjtype 4050 if org != "" || repo != "" || branch != "" || sha != "" { 4051 pj.Spec.Refs = &prowapi.Refs{ 4052 Org: org, 4053 Repo: repo, 4054 BaseRef: branch, 4055 BaseSHA: sha, 4056 Pulls: pulls, 4057 } 4058 } 4059 pj.Status.State = state 4060 return pj 4061 } 4062 4063 func newFakeManager(objs ...runtime.Object) *fakeManager { 4064 client := &indexingClient{ 4065 Client: fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(objs...).Build(), 4066 indexFuncs: map[string]ctrlruntimeclient.IndexerFunc{}, 4067 } 4068 return &fakeManager{ 4069 client: client, 4070 fakeFieldIndexer: &fakeFieldIndexer{ 4071 client: client, 4072 }, 4073 } 4074 } 4075 4076 type fakeManager struct { 4077 client *indexingClient 4078 *fakeFieldIndexer 4079 } 4080 4081 type fakeFieldIndexer struct { 4082 client *indexingClient 4083 } 4084 4085 func (fi *fakeFieldIndexer) IndexField(_ context.Context, _ ctrlruntimeclient.Object, field string, extractValue ctrlruntimeclient.IndexerFunc) error { 4086 fi.client.indexFuncs[field] = extractValue 4087 return nil 4088 } 4089 4090 func (fm *fakeManager) GetClient() ctrlruntimeclient.Client { 4091 return fm.client 4092 } 4093 4094 func (fm *fakeManager) GetFieldIndexer() ctrlruntimeclient.FieldIndexer { 4095 return fm.fakeFieldIndexer 4096 } 4097 4098 type indexingClient struct { 4099 ctrlruntimeclient.Client 4100 indexFuncs map[string]ctrlruntimeclient.IndexerFunc 4101 } 4102 4103 func (c *indexingClient) List(ctx context.Context, list ctrlruntimeclient.ObjectList, opts ...ctrlruntimeclient.ListOption) error { 4104 if err := c.Client.List(ctx, list, opts...); err != nil { 4105 return err 4106 } 4107 4108 listOpts := &ctrlruntimeclient.ListOptions{} 4109 for _, opt := range opts { 4110 opt.ApplyToList(listOpts) 4111 } 4112 4113 if listOpts.FieldSelector == nil { 4114 return nil 4115 } 4116 4117 if n := len(listOpts.FieldSelector.Requirements()); n == 0 { 4118 return nil 4119 } else if n > 1 { 4120 return fmt.Errorf("the indexing client supports at most one field selector requirement, got %d", n) 4121 } 4122 4123 indexKey := listOpts.FieldSelector.Requirements()[0].Field 4124 if indexKey == "" { 4125 return nil 4126 } 4127 4128 indexFunc, ok := c.indexFuncs[indexKey] 4129 if !ok { 4130 return fmt.Errorf("no index with key %q found", indexKey) 4131 } 4132 4133 pjList, ok := list.(*prowapi.ProwJobList) 4134 if !ok { 4135 return errors.New("indexes are only supported for ProwJobLists") 4136 } 4137 4138 result := prowapi.ProwJobList{} 4139 for _, pj := range pjList.Items { 4140 for _, indexVal := range indexFunc(&pj) { 4141 logrus.Infof("indexVal: %q, requirementVal: %q, match: %t", indexVal, listOpts.FieldSelector.Requirements()[0].Value, indexVal == listOpts.FieldSelector.Requirements()[0].Value) 4142 if indexVal == listOpts.FieldSelector.Requirements()[0].Value { 4143 result.Items = append(result.Items, pj) 4144 } 4145 } 4146 } 4147 4148 *pjList = result 4149 return nil 4150 } 4151 4152 func prowYAMLGetterForHeadRefs(headRefsToLookFor []string, ps []config.Presubmit) config.ProwYAMLGetter { 4153 return func(_ *config.Config, _ git.ClientFactory, _, _, _ string, headRefs ...string) (*config.ProwYAML, error) { 4154 if len(headRefsToLookFor) != len(headRefs) { 4155 return nil, fmt.Errorf("expcted %d headrefs, got %d", len(headRefsToLookFor), len(headRefs)) 4156 } 4157 var presubmits []config.Presubmit 4158 if sets.New[string](headRefsToLookFor...).Equal(sets.New[string](headRefs...)) { 4159 presubmits = ps 4160 } 4161 return &config.ProwYAML{ 4162 Presubmits: presubmits, 4163 }, nil 4164 } 4165 } 4166 4167 func TestNonFailedBatchByBaseAndPullsIndexFunc(t *testing.T) { 4168 successFullBatchJob := func(mods ...func(*prowapi.ProwJob)) *prowapi.ProwJob { 4169 pj := &prowapi.ProwJob{ 4170 Spec: prowapi.ProwJobSpec{ 4171 Type: prowapi.BatchJob, 4172 Job: "my-job", 4173 Refs: &prowapi.Refs{ 4174 Org: "org", 4175 Repo: "repo", 4176 BaseRef: "master", 4177 BaseSHA: "base-sha", 4178 Pulls: []prowapi.Pull{ 4179 { 4180 Number: 1, 4181 SHA: "1", 4182 }, 4183 { 4184 Number: 2, 4185 SHA: "2", 4186 }, 4187 }, 4188 }, 4189 }, 4190 Status: prowapi.ProwJobStatus{ 4191 State: prowapi.SuccessState, 4192 CompletionTime: &metav1.Time{}, 4193 }, 4194 } 4195 4196 for _, mod := range mods { 4197 mod(pj) 4198 } 4199 4200 return pj 4201 } 4202 const defaultIndexKey = "my-job|org|repo|master|base-sha|1|1|2|2" 4203 4204 testCases := []struct { 4205 name string 4206 pj *prowapi.ProwJob 4207 expected []string 4208 }{ 4209 { 4210 name: "Basic success", 4211 pj: successFullBatchJob(), 4212 expected: []string{defaultIndexKey}, 4213 }, 4214 { 4215 name: "Pulls reordered, same index", 4216 pj: successFullBatchJob(func(pj *prowapi.ProwJob) { 4217 pj.Spec.Refs.Pulls = []prowapi.Pull{ 4218 pj.Spec.Refs.Pulls[1], 4219 pj.Spec.Refs.Pulls[0], 4220 } 4221 }), 4222 expected: []string{defaultIndexKey}, 4223 }, 4224 { 4225 name: "Not completed, state is ignored", 4226 pj: successFullBatchJob(func(pj *prowapi.ProwJob) { 4227 pj.Status.CompletionTime = nil 4228 pj.Status.State = prowapi.TriggeredState 4229 }), 4230 expected: []string{defaultIndexKey}, 4231 }, 4232 { 4233 name: "Different name, different index", 4234 pj: successFullBatchJob(func(pj *prowapi.ProwJob) { 4235 pj.Spec.Job = "my-other-job" 4236 }), 4237 expected: []string{"my-other-job|org|repo|master|base-sha|1|1|2|2"}, 4238 }, 4239 { 4240 name: "Not a batch, ignored", 4241 pj: successFullBatchJob(func(pj *prowapi.ProwJob) { 4242 pj.Spec.Type = prowapi.PresubmitJob 4243 }), 4244 }, 4245 { 4246 name: "No refs, ignored", 4247 pj: successFullBatchJob(func(pj *prowapi.ProwJob) { 4248 pj.Spec.Refs = nil 4249 }), 4250 }, 4251 } 4252 4253 for _, tc := range testCases { 4254 result := nonFailedBatchByNameBaseAndPullsIndexFunc(tc.pj) 4255 if diff := deep.Equal(result, tc.expected); diff != nil { 4256 t.Errorf("Result differs from expected, diff: %v", diff) 4257 } 4258 } 4259 } 4260 4261 func TestCheckRunNodesToContexts(t *testing.T) { 4262 t.Parallel() 4263 testCases := []struct { 4264 name string 4265 checkRuns []CheckRun 4266 expected []Context 4267 }{ 4268 { 4269 name: "Empty checkrun is ignored", 4270 checkRuns: []CheckRun{{}}, 4271 }, 4272 { 4273 name: "Incomplete checkrun is considered pending", 4274 checkRuns: []CheckRun{{Name: githubql.String("some-job"), Status: githubql.String("queued")}}, 4275 expected: []Context{{Context: "some-job", State: githubql.StatusStatePending}}, 4276 }, 4277 { 4278 name: "Neutral checkrun is considered success", 4279 checkRuns: []CheckRun{{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.CheckConclusionStateNeutral)}}, 4280 expected: []Context{{Context: "some-job", State: githubql.StatusStateSuccess}}, 4281 }, 4282 { 4283 name: "Successful checkrun is considered success", 4284 checkRuns: []CheckRun{{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)}}, 4285 expected: []Context{{Context: "some-job", State: githubql.StatusStateSuccess}}, 4286 }, 4287 { 4288 name: "Other checkrun conclusion is considered failure", 4289 checkRuns: []CheckRun{{Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: "unclear"}}, 4290 expected: []Context{{Context: "some-job", State: githubql.StatusStateFailure}}, 4291 }, 4292 { 4293 name: "Multiple checkruns are translated correctly", 4294 checkRuns: []CheckRun{ 4295 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.CheckConclusionStateNeutral)}, 4296 {Name: githubql.String("another-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.CheckConclusionStateNeutral)}, 4297 }, 4298 expected: []Context{ 4299 {Context: "another-job", State: githubql.StatusStateSuccess}, 4300 {Context: "some-job", State: githubql.StatusStateSuccess}, 4301 }, 4302 }, 4303 { 4304 name: "De-duplicate checkruns, success > everything", 4305 checkRuns: []CheckRun{ 4306 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("FAILURE")}, 4307 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("ERROR")}, 4308 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted)}, 4309 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.StatusStateSuccess)}, 4310 }, 4311 expected: []Context{ 4312 {Context: "some-job", State: githubql.StatusStateSuccess}, 4313 }, 4314 }, 4315 { 4316 name: "De-duplicate checkruns, neutral > everything", 4317 checkRuns: []CheckRun{ 4318 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("FAILURE")}, 4319 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("ERROR")}, 4320 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted)}, 4321 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String(githubql.CheckConclusionStateNeutral)}, 4322 }, 4323 expected: []Context{ 4324 {Context: "some-job", State: githubql.StatusStateSuccess}, 4325 }, 4326 }, 4327 { 4328 name: "De-duplicate checkruns, pending > failure", 4329 checkRuns: []CheckRun{ 4330 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("FAILURE")}, 4331 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("ERROR")}, 4332 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted)}, 4333 {Name: githubql.String("some-job")}, 4334 }, 4335 expected: []Context{ 4336 {Context: "some-job", State: githubql.StatusStatePending}, 4337 }, 4338 }, 4339 { 4340 name: "De-duplicate checkruns, only failures", 4341 checkRuns: []CheckRun{ 4342 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("FAILURE")}, 4343 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted), Conclusion: githubql.String("ERROR")}, 4344 {Name: githubql.String("some-job"), Status: githubql.String(githubql.CheckStatusStateCompleted)}, 4345 }, 4346 expected: []Context{ 4347 {Context: "some-job", State: githubql.StatusStateFailure}, 4348 }, 4349 }, 4350 } 4351 4352 for _, tc := range testCases { 4353 t.Run(tc.name, func(t *testing.T) { 4354 // Shuffle the checkruns to make sure we don't rely on slice order 4355 rand.Shuffle(len(tc.checkRuns), func(i, j int) { 4356 tc.checkRuns[i], tc.checkRuns[j] = tc.checkRuns[j], tc.checkRuns[i] 4357 }) 4358 4359 var checkRunNodes []CheckRunNode 4360 for _, checkRun := range tc.checkRuns { 4361 checkRunNodes = append(checkRunNodes, CheckRunNode{CheckRun: checkRun}) 4362 } 4363 4364 result := checkRunNodesToContexts(logrus.New().WithField("test", tc.name), checkRunNodes) 4365 sort.Slice(result, func(i, j int) bool { 4366 return result[i].Context+result[i].Description+githubql.String(result[i].State) < result[j].Context+result[j].Description+githubql.String(result[j].State) 4367 }) 4368 4369 if diff := cmp.Diff(result, tc.expected); diff != "" { 4370 t.Errorf("actual result differs from expected: %s", diff) 4371 } 4372 }) 4373 } 4374 } 4375 4376 func TestDeduplicateContestsDoesntLoseData(t *testing.T) { 4377 seed := time.Now().UnixNano() 4378 // Print the seed so failures can easily be reproduced 4379 t.Logf("Seed: %d", seed) 4380 fuzzer := fuzz.NewWithSeed(seed) 4381 for i := 0; i < 100; i++ { 4382 t.Run(strconv.Itoa(i), func(t *testing.T) { 4383 context := Context{} 4384 fuzzer.Fuzz(&context) 4385 res := deduplicateContexts([]Context{context}) 4386 if diff := cmp.Diff(context, res[0]); diff != "" { 4387 t.Errorf("deduplicateContexts lost data, new object differs: %s", diff) 4388 } 4389 }) 4390 } 4391 } 4392 4393 func TestPickSmallestPassingNumber(t *testing.T) { 4394 priorities := []config.TidePriority{ 4395 {Labels: []string{"kind/failing-test"}}, 4396 {Labels: []string{"area/deflake"}}, 4397 {Labels: []string{"kind/bug", "priority/critical-urgent"}}, 4398 {Labels: []string{"kind/feature,kind/enhancement,kind/undefined"}}, 4399 } 4400 testCases := []struct { 4401 name string 4402 prs []CodeReviewCommon 4403 expected int 4404 }{ 4405 { 4406 name: "no label", 4407 prs: []CodeReviewCommon{ 4408 *CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)), 4409 *CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 3, githubql.MergeableStateMergeable)), 4410 }, 4411 expected: 3, 4412 }, 4413 { 4414 name: "any of given label alternatives", 4415 prs: []CodeReviewCommon{ 4416 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 3, githubql.MergeableStateMergeable, []string{"kind/enhancement", "kind/undefined"})), 4417 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 1, githubql.MergeableStateMergeable, []string{"kind/enhancement"})), 4418 }, 4419 expected: 1, 4420 }, 4421 { 4422 name: "deflake PR", 4423 prs: []CodeReviewCommon{ 4424 *CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)), 4425 *CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 3, githubql.MergeableStateMergeable)), 4426 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 7, githubql.MergeableStateMergeable, []string{"area/deflake"})), 4427 }, 4428 expected: 7, 4429 }, 4430 { 4431 name: "same label", 4432 prs: []CodeReviewCommon{ 4433 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 7, githubql.MergeableStateMergeable, []string{"area/deflake"})), 4434 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 6, githubql.MergeableStateMergeable, []string{"area/deflake"})), 4435 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 1, githubql.MergeableStateMergeable, []string{"area/deflake"})), 4436 }, 4437 expected: 1, 4438 }, 4439 { 4440 name: "missing one label", 4441 prs: []CodeReviewCommon{ 4442 *CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)), 4443 *CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 3, githubql.MergeableStateMergeable)), 4444 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 6, githubql.MergeableStateMergeable, []string{"kind/bug"})), 4445 }, 4446 expected: 3, 4447 }, 4448 { 4449 name: "complete", 4450 prs: []CodeReviewCommon{ 4451 *CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)), 4452 *CodeReviewCommonFromPullRequest(testPR("org", "repo", "A", 3, githubql.MergeableStateMergeable)), 4453 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 6, githubql.MergeableStateMergeable, []string{"kind/bug"})), 4454 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 7, githubql.MergeableStateMergeable, []string{"area/deflake"})), 4455 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 8, githubql.MergeableStateMergeable, []string{"kind/bug"})), 4456 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 9, githubql.MergeableStateMergeable, []string{"kind/failing-test"})), 4457 *CodeReviewCommonFromPullRequest(testPRWithLabels("org", "repo", "A", 10, githubql.MergeableStateMergeable, []string{"kind/bug", "priority/critical-urgent"})), 4458 }, 4459 expected: 9, 4460 }, 4461 } 4462 alwaysTrue := func(*logrus.Entry, *CodeReviewCommon, contextChecker) bool { return true } 4463 for _, tc := range testCases { 4464 t.Run(tc.name, func(t *testing.T) { 4465 _, got := pickHighestPriorityPR(nil, tc.prs, nil, alwaysTrue, priorities) 4466 if int(got.Number) != tc.expected { 4467 t.Errorf("got %d, expected %d", int(got.Number), tc.expected) 4468 } 4469 }) 4470 } 4471 } 4472 4473 func TestQueryShardsByOrgWhenAppsAuthIsEnabledOnly(t *testing.T) { 4474 t.Parallel() 4475 4476 testCases := []struct { 4477 name string 4478 usesGitHubAppsAuth bool 4479 prs map[string][]PullRequest 4480 expectedNumberOfApiCalls int 4481 }{ 4482 { 4483 name: "Apps auth is used, one call per org", 4484 usesGitHubAppsAuth: true, 4485 prs: map[string][]PullRequest{ 4486 "org": {*testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable)}, 4487 "other-org": {*testPR("other-org", "repo", "A", 5, githubql.MergeableStateMergeable)}, 4488 }, 4489 expectedNumberOfApiCalls: 2, 4490 }, 4491 { 4492 name: "Apps auth is unused, one call for all orgs", 4493 usesGitHubAppsAuth: false, 4494 prs: map[string][]PullRequest{"": { 4495 *testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable), 4496 *testPR("other-org", "repo", "A", 5, githubql.MergeableStateMergeable), 4497 }}, 4498 expectedNumberOfApiCalls: 1, 4499 }, 4500 } 4501 4502 for _, tc := range testCases { 4503 t.Run(tc.name, func(t *testing.T) { 4504 provider := &GitHubProvider{ 4505 cfg: func() *config.Config { 4506 return &config.Config{ProwConfig: config.ProwConfig{Tide: config.Tide{ 4507 TideGitHubConfig: config.TideGitHubConfig{Queries: []config.TideQuery{{Orgs: []string{"org", "other-org"}}}}}}} 4508 }, 4509 ghc: &fgc{prs: tc.prs}, 4510 usesGitHubAppsAuth: tc.usesGitHubAppsAuth, 4511 logger: logrus.WithField("test", tc.name), 4512 } 4513 4514 prs, err := provider.Query() 4515 if err != nil { 4516 t.Fatalf("query() failed: %v", err) 4517 } 4518 if n := len(prs); n != 2 { 4519 t.Errorf("expected to get two prs back, got %d", n) 4520 } 4521 if diff := cmp.Diff(tc.expectedNumberOfApiCalls, provider.ghc.(*fgc).queryCalls); diff != "" { 4522 t.Errorf("expectedNumberOfApiCallsByOrg differs from actual: %s", diff) 4523 } 4524 }) 4525 } 4526 } 4527 4528 func TestPickBatchPrefersBatchesWithPreexistingJobs(t *testing.T) { 4529 t.Parallel() 4530 const org, repo = "org", "repo" 4531 tests := []struct { 4532 name string 4533 subpool func(*subpool) 4534 prsFailingContextCheck sets.Set[int] 4535 maxBatchSize int 4536 prioritizeExistingBatchesMap map[string]bool 4537 4538 expectedPullRequests []CodeReviewCommon 4539 }{ 4540 { 4541 name: "No pre-existing jobs, new batch is picked", 4542 subpool: func(sp *subpool) { sp.pjs = nil }, 4543 expectedPullRequests: []CodeReviewCommon{ 4544 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}), 4545 }, 4546 }, 4547 { 4548 name: "Batch with pre-existing success jobs exists and is picked", 4549 subpool: func(sp *subpool) {}, 4550 expectedPullRequests: []CodeReviewCommon{ 4551 *CodeReviewCommonFromPullRequest(&PullRequest{ 4552 Number: githubql.Int(1), 4553 HeadRefOID: githubql.String("1"), 4554 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4555 Nodes: []struct{ Commit Commit }{ 4556 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "1"}}}, 4557 }, 4558 }), 4559 *CodeReviewCommonFromPullRequest(&PullRequest{ 4560 Number: githubql.Int(2), 4561 HeadRefOID: githubql.String("2"), 4562 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4563 Nodes: []struct{ Commit Commit }{ 4564 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "2"}}}, 4565 }, 4566 }), 4567 *CodeReviewCommonFromPullRequest(&PullRequest{ 4568 Number: githubql.Int(3), 4569 HeadRefOID: githubql.String("3"), 4570 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4571 Nodes: []struct{ Commit Commit }{ 4572 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "3"}}}, 4573 }, 4574 }), 4575 *CodeReviewCommonFromPullRequest(&PullRequest{ 4576 Number: githubql.Int(4), 4577 HeadRefOID: githubql.String("4"), 4578 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4579 Nodes: []struct{ Commit Commit }{ 4580 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "4"}}}, 4581 }, 4582 }), 4583 }, 4584 }, 4585 { 4586 name: "Batch with pre-existing success jobs exists but PrioritizeExistingBatches is disabled globally, new batch is picked", 4587 subpool: func(sp *subpool) {}, 4588 prioritizeExistingBatchesMap: map[string]bool{"*": false}, 4589 expectedPullRequests: []CodeReviewCommon{ 4590 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}), 4591 }, 4592 }, 4593 { 4594 name: "Batch with pre-existing success jobs exists but PrioritizeExistingBatches is disabled for org, new batch is picked", 4595 subpool: func(sp *subpool) {}, 4596 prioritizeExistingBatchesMap: map[string]bool{org: false}, 4597 expectedPullRequests: []CodeReviewCommon{ 4598 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}), 4599 }, 4600 }, 4601 { 4602 name: "Batch with pre-existing success jobs exists but PrioritizeExistingBatches is disabled for repo, new batch is picked", 4603 subpool: func(sp *subpool) {}, 4604 prioritizeExistingBatchesMap: map[string]bool{org + "/" + repo: false}, 4605 expectedPullRequests: []CodeReviewCommon{ 4606 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}), 4607 }, 4608 }, 4609 { 4610 name: "Batch with pre-existing success job exists but one fails context check, new batch is picked", 4611 subpool: func(sp *subpool) {}, 4612 prsFailingContextCheck: sets.New[int](1), 4613 expectedPullRequests: []CodeReviewCommon{ 4614 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}), 4615 }, 4616 }, 4617 { 4618 name: "Batch with pre-existing success job exists but is bigger than maxBatchSize, new batch is picked", 4619 subpool: func(sp *subpool) {}, 4620 maxBatchSize: 3, 4621 expectedPullRequests: []CodeReviewCommon{ 4622 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}), 4623 }, 4624 }, 4625 { 4626 name: "Batch with pre-existing success job exists but one PR is outdated, new batch is picked", 4627 subpool: func(sp *subpool) { sp.prs[0].HeadRefOID = "new-sha" }, 4628 expectedPullRequests: []CodeReviewCommon{ 4629 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}), 4630 }, 4631 }, 4632 { 4633 name: "Batchjobs exist but is failed, new batch is picked", 4634 subpool: func(sp *subpool) { sp.pjs[0].Status.State = prowapi.FailureState }, 4635 expectedPullRequests: []CodeReviewCommon{ 4636 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"}), 4637 }, 4638 }, 4639 { 4640 name: "Batch with pre-existing success jobs and batch with pre-existing pending jobs exists, batch with success jobs is picked", 4641 subpool: func(sp *subpool) { 4642 sp.pjs = append(sp.pjs, *sp.pjs[0].DeepCopy()) 4643 sp.pjs[0].Spec.Refs.Pulls = []prowapi.Pull{{Number: 1, SHA: "1"}, {Number: 2, SHA: "2"}} 4644 4645 sp.pjs[1].Status.State = prowapi.PendingState 4646 sp.pjs[1].Spec.Refs.Pulls = []prowapi.Pull{{Number: 3, SHA: "3"}, {Number: 4, SHA: "4"}} 4647 }, 4648 expectedPullRequests: []CodeReviewCommon{ 4649 *CodeReviewCommonFromPullRequest(&PullRequest{ 4650 Number: githubql.Int(1), 4651 HeadRefOID: githubql.String("1"), 4652 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4653 Nodes: []struct{ Commit Commit }{ 4654 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "1"}}}, 4655 }, 4656 }), 4657 *CodeReviewCommonFromPullRequest(&PullRequest{ 4658 Number: githubql.Int(2), 4659 HeadRefOID: githubql.String("2"), 4660 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4661 Nodes: []struct{ Commit Commit }{ 4662 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "2"}}}, 4663 }, 4664 }), 4665 }, 4666 }, 4667 { 4668 name: "Batch with pre-existing pending jobs exists and is picked", 4669 subpool: func(sp *subpool) { sp.pjs[0].Status.State = prowapi.PendingState }, 4670 expectedPullRequests: []CodeReviewCommon{ 4671 *CodeReviewCommonFromPullRequest(&PullRequest{ 4672 Number: githubql.Int(1), 4673 HeadRefOID: githubql.String("1"), 4674 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4675 Nodes: []struct{ Commit Commit }{ 4676 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "1"}}}, 4677 }, 4678 }), 4679 *CodeReviewCommonFromPullRequest(&PullRequest{ 4680 Number: githubql.Int(2), 4681 HeadRefOID: githubql.String("2"), 4682 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4683 Nodes: []struct{ Commit Commit }{ 4684 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "2"}}}, 4685 }, 4686 }), 4687 *CodeReviewCommonFromPullRequest(&PullRequest{ 4688 Number: githubql.Int(3), 4689 HeadRefOID: githubql.String("3"), 4690 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4691 Nodes: []struct{ Commit Commit }{ 4692 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "3"}}}, 4693 }, 4694 }), 4695 *CodeReviewCommonFromPullRequest(&PullRequest{ 4696 Number: githubql.Int(4), 4697 HeadRefOID: githubql.String("4"), 4698 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4699 Nodes: []struct{ Commit Commit }{ 4700 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "4"}}}, 4701 }, 4702 }), 4703 }, 4704 }, 4705 { 4706 name: "Multiple success batches exists, the one with the highest number of tests is picked", 4707 subpool: func(sp *subpool) { 4708 sp.pjs = append(sp.pjs, *sp.pjs[0].DeepCopy(), *sp.pjs[0].DeepCopy()) 4709 4710 sp.pjs[1].Spec.Refs.Pulls = []prowapi.Pull{{Number: 3, SHA: "3"}, {Number: 4, SHA: "4"}} 4711 sp.pjs[2].Spec.Refs.Pulls = []prowapi.Pull{{Number: 3, SHA: "3"}, {Number: 4, SHA: "4"}} 4712 }, 4713 expectedPullRequests: []CodeReviewCommon{ 4714 *CodeReviewCommonFromPullRequest(&PullRequest{ 4715 Number: githubql.Int(3), 4716 HeadRefOID: githubql.String("3"), 4717 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4718 Nodes: []struct{ Commit Commit }{ 4719 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "3"}}}, 4720 }, 4721 }), 4722 *CodeReviewCommonFromPullRequest(&PullRequest{ 4723 Number: githubql.Int(4), 4724 HeadRefOID: githubql.String("4"), 4725 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4726 Nodes: []struct{ Commit Commit }{ 4727 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "4"}}}, 4728 }, 4729 }), 4730 }, 4731 }, 4732 { 4733 name: "Multiple pending batches exist, the one with the highest number of tests is picked", 4734 subpool: func(sp *subpool) { 4735 sp.pjs[0].Status.State = prowapi.PendingState 4736 sp.pjs = append(sp.pjs, *sp.pjs[0].DeepCopy(), *sp.pjs[0].DeepCopy()) 4737 4738 sp.pjs[1].Spec.Refs.Pulls = []prowapi.Pull{{Number: 3, SHA: "3"}, {Number: 4, SHA: "4"}} 4739 sp.pjs[2].Spec.Refs.Pulls = []prowapi.Pull{{Number: 3, SHA: "3"}, {Number: 4, SHA: "4"}} 4740 }, 4741 expectedPullRequests: []CodeReviewCommon{ 4742 *CodeReviewCommonFromPullRequest(&PullRequest{ 4743 Number: githubql.Int(3), 4744 HeadRefOID: githubql.String("3"), 4745 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4746 Nodes: []struct{ Commit Commit }{ 4747 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "3"}}}, 4748 }, 4749 }), 4750 *CodeReviewCommonFromPullRequest(&PullRequest{ 4751 Number: githubql.Int(4), 4752 HeadRefOID: githubql.String("4"), 4753 Commits: struct{ Nodes []struct{ Commit Commit } }{ 4754 Nodes: []struct{ Commit Commit }{ 4755 {Commit: Commit{Status: CommitStatus{Contexts: []Context{}}, OID: "4"}}}, 4756 }, 4757 }), 4758 }, 4759 }, 4760 } 4761 4762 for _, tc := range tests { 4763 t.Run(tc.name, func(t *testing.T) { 4764 sp := subpool{ 4765 org: org, 4766 repo: repo, 4767 log: logrus.WithField("test", tc.name), 4768 prs: []CodeReviewCommon{ 4769 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 1, HeadRefOID: "1"}), 4770 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 2, HeadRefOID: "2"}), 4771 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 3, HeadRefOID: "3"}), 4772 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 4, HeadRefOID: "4"}), 4773 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 5, HeadRefOID: "5"}), 4774 }, 4775 pjs: []prowapi.ProwJob{{ 4776 Spec: prowapi.ProwJobSpec{ 4777 Refs: &prowapi.Refs{Pulls: []prowapi.Pull{ 4778 {Number: 1, SHA: "1"}, 4779 {Number: 2, SHA: "2"}, 4780 {Number: 3, SHA: "3"}, 4781 {Number: 4, SHA: "4"}, 4782 }}, 4783 Type: prowapi.BatchJob, 4784 }, 4785 Status: prowapi.ProwJobStatus{ 4786 State: prowapi.SuccessState, 4787 }, 4788 }}, 4789 } 4790 tc.subpool(&sp) 4791 4792 contextCheckers := make(map[int]contextChecker, len(sp.prs)) 4793 for _, pr := range sp.prs { 4794 cc := &config.TideContextPolicy{} 4795 if tc.prsFailingContextCheck.Has(int(pr.Number)) { 4796 cc.RequiredContexts = []string{"guaranteed-absent"} 4797 } 4798 contextCheckers[int(pr.Number)] = cc 4799 } 4800 4801 newBatchFunc := func(sp subpool, candidates []CodeReviewCommon, maxBatchSize int) ([]CodeReviewCommon, error) { 4802 return []CodeReviewCommon{ 4803 *CodeReviewCommonFromPullRequest(&PullRequest{Number: 99, HeadRefOID: "pr-from-new-batch-func"})}, nil 4804 } 4805 4806 cfg := func() *config.Config { 4807 return &config.Config{ProwConfig: config.ProwConfig{ 4808 Tide: config.Tide{ 4809 BatchSizeLimitMap: map[string]int{"*": tc.maxBatchSize}, 4810 PrioritizeExistingBatchesMap: tc.prioritizeExistingBatchesMap, 4811 }}, 4812 } 4813 } 4814 4815 logger := logrus.WithField("test", tc.name) 4816 ghc := &fgc{skipExpectedShaCheck: true} 4817 c := &syncController{ 4818 logger: logrus.WithField("test", tc.name), 4819 config: cfg, 4820 provider: &GitHubProvider{ 4821 cfg: cfg, 4822 logger: logger, 4823 ghc: ghc, 4824 }, 4825 } 4826 prs, _, err := c.pickBatch(sp, contextCheckers, newBatchFunc) 4827 if err != nil { 4828 t.Fatalf("pickBatch failed: %v", err) 4829 } 4830 if diff := cmp.Diff(tc.expectedPullRequests, prs); diff != "" { 4831 t.Errorf("expected pull requests differ from actual: %s", diff) 4832 } 4833 }) 4834 4835 } 4836 } 4837 4838 func TestTenantIDs(t *testing.T) { 4839 tests := []struct { 4840 name string 4841 pjs []prowapi.ProwJob 4842 expected []string 4843 }{ 4844 { 4845 name: "no PJs", 4846 pjs: []prowapi.ProwJob{}, 4847 expected: []string{}, 4848 }, 4849 { 4850 name: "one PJ", 4851 pjs: []prowapi.ProwJob{ 4852 { 4853 Spec: prowapi.ProwJobSpec{ 4854 ProwJobDefault: &prowapi.ProwJobDefault{ 4855 TenantID: "test", 4856 }, 4857 }, 4858 }, 4859 }, 4860 expected: []string{"test"}, 4861 }, 4862 { 4863 name: "multiple PJs with same ID", 4864 pjs: []prowapi.ProwJob{ 4865 { 4866 Spec: prowapi.ProwJobSpec{ 4867 ProwJobDefault: &prowapi.ProwJobDefault{ 4868 TenantID: "test", 4869 }, 4870 }, 4871 }, 4872 { 4873 Spec: prowapi.ProwJobSpec{ 4874 ProwJobDefault: &prowapi.ProwJobDefault{ 4875 TenantID: "test", 4876 }, 4877 }, 4878 }, 4879 }, 4880 expected: []string{"test"}, 4881 }, 4882 { 4883 name: "multiple PJs with different ID", 4884 pjs: []prowapi.ProwJob{ 4885 { 4886 Spec: prowapi.ProwJobSpec{ 4887 ProwJobDefault: &prowapi.ProwJobDefault{ 4888 TenantID: "test", 4889 }, 4890 }, 4891 }, 4892 { 4893 Spec: prowapi.ProwJobSpec{ 4894 ProwJobDefault: &prowapi.ProwJobDefault{ 4895 TenantID: "other", 4896 }, 4897 }, 4898 }, 4899 }, 4900 expected: []string{"test", "other"}, 4901 }, 4902 { 4903 name: "no tenantID in prowJob", 4904 pjs: []prowapi.ProwJob{ 4905 { 4906 Spec: prowapi.ProwJobSpec{ 4907 ProwJobDefault: &prowapi.ProwJobDefault{ 4908 TenantID: "test", 4909 }, 4910 }, 4911 }, 4912 { 4913 Spec: prowapi.ProwJobSpec{ 4914 ProwJobDefault: &prowapi.ProwJobDefault{}, 4915 }, 4916 }, 4917 }, 4918 expected: []string{"test", ""}, 4919 }, 4920 { 4921 name: "no pjDefault in prowJob", 4922 pjs: []prowapi.ProwJob{ 4923 { 4924 Spec: prowapi.ProwJobSpec{ 4925 ProwJobDefault: &prowapi.ProwJobDefault{ 4926 TenantID: "test", 4927 }, 4928 }, 4929 }, 4930 { 4931 Spec: prowapi.ProwJobSpec{}, 4932 }, 4933 }, 4934 expected: []string{"test", ""}, 4935 }, 4936 { 4937 name: "multiple no tenant PJs", 4938 pjs: []prowapi.ProwJob{ 4939 { 4940 Spec: prowapi.ProwJobSpec{ 4941 ProwJobDefault: &prowapi.ProwJobDefault{ 4942 TenantID: "", 4943 }, 4944 }, 4945 }, 4946 { 4947 Spec: prowapi.ProwJobSpec{}, 4948 }, 4949 }, 4950 expected: []string{""}, 4951 }, 4952 } 4953 for _, tc := range tests { 4954 t.Run(tc.name, func(t *testing.T) { 4955 sp := subpool{pjs: tc.pjs} 4956 if diff := cmp.Diff(tc.expected, sp.TenantIDs(), cmpopts.SortSlices(func(x, y string) bool { return strings.Compare(x, y) > 0 })); diff != "" { 4957 t.Errorf("expected tenantIDs differ from actual: %s", diff) 4958 } 4959 }) 4960 } 4961 } 4962 4963 func TestSetTideStatusSuccess(t *testing.T) { 4964 t.Parallel() 4965 testCases := []struct { 4966 name string 4967 pr PullRequest 4968 4969 expectApiCall bool 4970 }{ 4971 { 4972 name: "Status is set", 4973 expectApiCall: true, 4974 }, 4975 { 4976 name: "PR already has tide status set to success, no api call is made", 4977 pr: PullRequest{Commits: struct{ Nodes []struct{ Commit Commit } }{Nodes: []struct{ Commit Commit }{{Commit: Commit{Status: CommitStatus{Contexts: []Context{{Context: "tide", State: githubql.StatusState("success")}}}}}}}}, 4978 }, 4979 } 4980 4981 for _, tc := range testCases { 4982 t.Run(tc.name, func(t *testing.T) { 4983 ghc := &fgc{} 4984 crc := CodeReviewCommonFromPullRequest(&tc.pr) 4985 err := setTideStatusSuccess(*crc, ghc, &config.Config{}, logrus.WithField("test", tc.name)) 4986 if err != nil { 4987 t.Fatalf("failed to set status: %v", err) 4988 } 4989 4990 if ghc.setStatus != tc.expectApiCall { 4991 t.Errorf("expected CreateStatusApiCall: %t, got CreateStatusApiCall: %t", tc.expectApiCall, ghc.setStatus) 4992 } 4993 }) 4994 } 4995 } 4996 4997 // TestBatchPickingConsidersPRThatIsCurrentlyBeingSeriallyRetested verifies the following sequence of events: 4998 // 1. Tide creates a serial retest run for a passing PR 4999 // 2. The status contexts on the PR get updated to pending 5000 // 3. A second PR becomes eligible 5001 // 4. Tide creates a batch of the first and the second PR 5002 func TestBatchPickingConsidersPRThatIsCurrentlyBeingSeriallyRetested(t *testing.T) { 5003 t.Parallel() 5004 configGetter := func() *config.Config { 5005 return &config.Config{ 5006 ProwConfig: config.ProwConfig{ 5007 Tide: config.Tide{ 5008 MaxGoroutines: 1, 5009 TideGitHubConfig: config.TideGitHubConfig{ 5010 Queries: config.TideQueries{{}}, 5011 }, 5012 }, 5013 }, 5014 JobConfig: config.JobConfig{PresubmitsStatic: map[string][]config.Presubmit{ 5015 "/": {{AlwaysRun: true, Reporter: config.Reporter{Context: "mandatory-job"}}}, 5016 }}, 5017 } 5018 } 5019 ghc := &fgc{} 5020 mmc := newMergeChecker(configGetter, ghc) 5021 mgr := newFakeManager() 5022 log := logrus.WithField("test", t.Name()) 5023 history, err := history.New(1, nil, "") 5024 if err != nil { 5025 t.Fatalf("failed to construct history: %v", err) 5026 } 5027 ghProvider := newGitHubProvider(log, ghc, nil, configGetter, mmc, false) 5028 c, err := newSyncController( 5029 context.Background(), 5030 log, 5031 mgr, 5032 ghProvider, 5033 configGetter, 5034 nil, 5035 history, 5036 false, 5037 &statusUpdate{ 5038 dontUpdateStatus: &threadSafePRSet{}, 5039 newPoolPending: make(chan bool), 5040 }, 5041 ) 5042 if err != nil { 5043 t.Fatalf("failed to construct sync controller: %v", err) 5044 } 5045 c.pickNewBatch = func(sp subpool, candidates []CodeReviewCommon, maxBatchSize int) ([]CodeReviewCommon, error) { 5046 return candidates, nil 5047 } 5048 5049 // Add a successful PR to github 5050 initialPR := PullRequest{} 5051 initialPR.Commits.Nodes = append(initialPR.Commits.Nodes, struct{ Commit Commit }{ 5052 Commit: Commit{Status: CommitStatus{Contexts: []Context{ 5053 { 5054 Context: githubql.String("mandatory-job"), 5055 State: githubql.StatusStateSuccess, 5056 }, 5057 { 5058 Context: githubql.String(statusContext), 5059 State: githubql.StatusStatePending, 5060 }, 5061 }}}, 5062 }) 5063 ghc.prs = map[string][]PullRequest{"": {initialPR}} 5064 5065 // sync, this creates a new serial retest prowjob 5066 if err := c.Sync(); err != nil { 5067 t.Fatalf("sync failed: %v", err) 5068 } 5069 // Ensure there is actually the retest job 5070 var pjs prowapi.ProwJobList 5071 if err := c.prowJobClient.List(c.ctx, &pjs); err != nil { 5072 t.Fatalf("failed to list prowjobs: %v", err) 5073 } 5074 if n := len(pjs.Items); n != 1 { 5075 t.Fatalf("expected a prowjob to be created, but client had %d items", n) 5076 } 5077 5078 // Update the context on the PR to pending just like crier would 5079 for idx, ctx := range initialPR.Commits.Nodes[0].Commit.Status.Contexts { 5080 if pjs.Items[0].Spec.Context == string(ctx.Context) { 5081 initialPR.Commits.Nodes[0].Commit.Status.Contexts[idx].State = githubql.StatusStatePending 5082 } 5083 } 5084 5085 // Add a second PR that also needs retesting to GitHub 5086 secondPR := PullRequest{Number: githubql.Int(1)} 5087 secondPR.Commits.Nodes = append(secondPR.Commits.Nodes, struct{ Commit Commit }{ 5088 Commit: Commit{Status: CommitStatus{Contexts: []Context{ 5089 { 5090 Context: githubql.String("mandatory-job"), 5091 State: githubql.StatusStateSuccess, 5092 }, 5093 { 5094 Context: githubql.String(statusContext), 5095 State: githubql.StatusStatePending, 5096 }, 5097 }}}, 5098 }) 5099 ghc.prs[""] = append(ghc.prs[""], secondPR) 5100 5101 // sync again 5102 if err := c.Sync(); err != nil { 5103 t.Fatalf("failed to sync: %v", err) 5104 } 5105 5106 // verify we have a batch prowjob 5107 if err := c.prowJobClient.List(c.ctx, &pjs); err != nil { 5108 t.Fatalf("failed to list prowjobs: %v", err) 5109 } 5110 for _, pj := range pjs.Items { 5111 if pj.Spec.Type == prowapi.BatchJob { 5112 return 5113 } 5114 } 5115 5116 t.Errorf("expected to find a batch prwjob, but wasn't the case. ProwJobs: %+v", pjs.Items) 5117 } 5118 5119 func TestIsBatchCandidateEligible(t *testing.T) { 5120 t.Parallel() 5121 5122 const ( 5123 requiredContextName = "required-context" 5124 optionalContextName = "optional-context" 5125 ) 5126 5127 tcs := []struct { 5128 name string 5129 pjManipulator func(**prowapi.ProwJob) 5130 prManipulator func(*PullRequest) 5131 5132 expected bool 5133 }{ 5134 { 5135 name: "Is eligible", 5136 expected: true, 5137 }, 5138 { 5139 name: "Successful context doesn't require prowjob", 5140 prManipulator: func(pr *PullRequest) { 5141 pr.Commits.Nodes[0].Commit.Status.Contexts[0].State = githubql.StatusStateSuccess 5142 }, 5143 pjManipulator: func(pj **prowapi.ProwJob) { *pj = nil }, 5144 expected: true, 5145 }, 5146 { 5147 name: "Optional failed context is ignored", 5148 expected: true, 5149 prManipulator: func(pr *PullRequest) { 5150 pr.Commits.Nodes[0].Commit.Status.Contexts = append(pr.Commits.Nodes[0].Commit.Status.Contexts, Context{ 5151 Context: githubql.String(optionalContextName), 5152 State: githubql.StatusStateFailure, 5153 }) 5154 }, 5155 }, 5156 { 5157 name: "Tides own context is ignored", 5158 expected: true, 5159 prManipulator: func(pr *PullRequest) { 5160 pr.Commits.Nodes[0].Commit.Status.Contexts = append(pr.Commits.Nodes[0].Commit.Status.Contexts, Context{ 5161 Context: githubql.String(statusContext), 5162 State: githubql.StatusStateFailure, 5163 }) 5164 }, 5165 }, 5166 { 5167 name: "Has missing required context, not eligible", 5168 prManipulator: func(pr *PullRequest) { pr.Commits.Nodes[0].Commit.Status.Contexts = nil }, 5169 }, 5170 { 5171 name: "Has failed context, not eligible", 5172 prManipulator: func(pr *PullRequest) { 5173 pr.Commits.Nodes[0].Commit.Status.Contexts[0].State = githubql.StatusStateFailure 5174 }, 5175 }, 5176 { 5177 name: "Has error context, not eligible", 5178 prManipulator: func(pr *PullRequest) { 5179 pr.Commits.Nodes[0].Commit.Status.Contexts[0].State = githubql.StatusStateError 5180 }, 5181 }, 5182 { 5183 name: "No prowjob, not eligible", 5184 pjManipulator: func(pj **prowapi.ProwJob) { *pj = nil }, 5185 }, 5186 { 5187 name: "Pj doesn't have created by tide label, not eligible", 5188 pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels["created-by-tide"] = "wrong" }, 5189 }, 5190 { 5191 name: "Pj doesn't have presubmit label, not eligible", 5192 pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.ProwJobTypeLabel] = "wrong" }, 5193 }, 5194 { 5195 name: "PJ doesn't have org label, not eligible", 5196 pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.OrgLabel] = "wrong" }, 5197 }, 5198 { 5199 name: "PJ doesn't have repo label, not eligible", 5200 pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.RepoLabel] = "wrong" }, 5201 }, 5202 { 5203 name: "Pj doesn't have baseref label, not eligible", 5204 pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.BaseRefLabel] = "wrong" }, 5205 }, 5206 { 5207 name: "Pj doesn't have pull label, not eligible", 5208 pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.PullLabel] = "wrong" }, 5209 }, 5210 { 5211 name: "pj doesn't have context label, not eligible", 5212 pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Labels[kube.ContextAnnotation] = "wrong" }, 5213 }, 5214 { 5215 name: "Pj is for wrong headref, not eligible", 5216 pjManipulator: func(pj **prowapi.ProwJob) { (*pj).Spec.Refs.Pulls[0].SHA = "wrong" }, 5217 }, 5218 } 5219 5220 newPR := func() PullRequest { 5221 pr := PullRequest{} 5222 pr.Commits.Nodes = append(pr.Commits.Nodes, struct{ Commit Commit }{Commit: Commit{Status: CommitStatus{Contexts: []Context{ 5223 {Context: githubql.String(requiredContextName), State: githubql.StatusStatePending}, 5224 }}}}) 5225 return pr 5226 } 5227 newProwJob := func() *prowapi.ProwJob { 5228 return &prowapi.ProwJob{ 5229 ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{ 5230 "created-by-tide": "true", 5231 kube.ProwJobTypeLabel: "presubmit", 5232 kube.OrgLabel: "", 5233 kube.RepoLabel: "", 5234 kube.BaseRefLabel: "", 5235 kube.PullLabel: "0", 5236 kube.ContextAnnotation: requiredContextName, 5237 }}, 5238 Spec: prowapi.ProwJobSpec{Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}}}, 5239 } 5240 } 5241 5242 for _, tc := range tcs { 5243 t.Run(tc.name, func(t *testing.T) { 5244 pj := newProwJob() 5245 pr := newPR() 5246 5247 if tc.prManipulator != nil { 5248 tc.prManipulator(&pr) 5249 } 5250 if tc.pjManipulator != nil { 5251 tc.pjManipulator(&pj) 5252 } 5253 5254 builder := fakectrlruntimeclient.NewClientBuilder() 5255 if pj != nil { 5256 builder.WithRuntimeObjects(pj) 5257 } 5258 5259 cfg := func() *config.Config { return &config.Config{} } 5260 c := &syncController{ 5261 config: cfg, 5262 provider: &GitHubProvider{cfg: cfg}, 5263 ctx: context.Background(), 5264 prowJobClient: builder.Build(), 5265 } 5266 5267 cc := &config.TideContextPolicy{ 5268 RequiredContexts: []string{requiredContextName}, 5269 OptionalContexts: []string{optionalContextName}, 5270 } 5271 5272 if actual := c.isRetestEligible(logrus.WithField("tc", tc.name), CodeReviewCommonFromPullRequest(&pr), cc); actual != tc.expected { 5273 t.Errorf("expected result %t, got %t", tc.expected, actual) 5274 } 5275 }) 5276 } 5277 } 5278 5279 // TestSerialRetestingConsidersPRThatIsCurrentlyBeingSRetested verifies the following sequence of events: 5280 // 1. Tide creates a serial retest run for a passing PR 5281 // 2. The status context on the PR gets updated to pending 5282 // 3. Another PR gets merged and changed the baseSHA, for example because it already had up-to-date tests but was missing labels 5283 // 4. Tide will again trigger serial retests for the passing PR (The runs from step 1 will be deleted by Plank) 5284 func TestSerialRetestingConsidersPRThatIsCurrentlyBeingSRetested(t *testing.T) { 5285 t.Parallel() 5286 configGetter := func() *config.Config { 5287 return &config.Config{ 5288 ProwConfig: config.ProwConfig{ 5289 Tide: config.Tide{ 5290 MaxGoroutines: 1, 5291 TideGitHubConfig: config.TideGitHubConfig{ 5292 Queries: config.TideQueries{{}}, 5293 }, 5294 }, 5295 }, 5296 JobConfig: config.JobConfig{PresubmitsStatic: map[string][]config.Presubmit{ 5297 "/": {{AlwaysRun: true, Reporter: config.Reporter{Context: "mandatory-job"}}}, 5298 }}, 5299 } 5300 } 5301 ghc := &fgc{} 5302 mmc := newMergeChecker(configGetter, ghc) 5303 mgr := newFakeManager() 5304 log := logrus.WithField("test", t.Name()) 5305 history, err := history.New(1, nil, "") 5306 if err != nil { 5307 t.Fatalf("failed to construct history: %v", err) 5308 } 5309 ghProvider := newGitHubProvider(log, ghc, nil, configGetter, mmc, false) 5310 c, err := newSyncController( 5311 context.Background(), 5312 log, 5313 mgr, 5314 ghProvider, 5315 configGetter, 5316 nil, 5317 history, 5318 false, 5319 &statusUpdate{ 5320 dontUpdateStatus: &threadSafePRSet{}, 5321 newPoolPending: make(chan bool), 5322 }, 5323 ) 5324 if err != nil { 5325 t.Fatalf("failed to construct sync controller: %v", err) 5326 } 5327 5328 // Add a successful PR to github 5329 initialPR := PullRequest{} 5330 initialPR.Commits.Nodes = append(initialPR.Commits.Nodes, struct{ Commit Commit }{ 5331 Commit: Commit{Status: CommitStatus{Contexts: []Context{ 5332 { 5333 Context: githubql.String("mandatory-job"), 5334 State: githubql.StatusStateSuccess, 5335 }, 5336 { 5337 Context: githubql.String(statusContext), 5338 State: githubql.StatusStatePending, 5339 }, 5340 }}}, 5341 }) 5342 ghc.prs = map[string][]PullRequest{"": {initialPR}} 5343 5344 // sync, this creates a new serial retest prowjob 5345 if err := c.Sync(); err != nil { 5346 t.Fatalf("sync failed: %v", err) 5347 } 5348 // ensure there is actually the retest job 5349 var pjs prowapi.ProwJobList 5350 if err := c.prowJobClient.List(c.ctx, &pjs); err != nil { 5351 t.Fatalf("failed to list prowjobs: %v", err) 5352 } 5353 if n := len(pjs.Items); n != 1 { 5354 t.Errorf("expected to find exactly one prowjob, got %d from list %+v", n, pjs) 5355 } 5356 5357 // Update the context on the PR to pending just like crier would 5358 for idx, ctx := range initialPR.Commits.Nodes[0].Commit.Status.Contexts { 5359 if pjs.Items[0].Spec.Context == string(ctx.Context) { 5360 initialPR.Commits.Nodes[0].Commit.Status.Contexts[idx].State = githubql.StatusStatePending 5361 } 5362 } 5363 5364 // Update the sha of the pool 5365 ghc.refs = map[string]string{"/ ": "new-base-sha"} 5366 5367 // sync, this creates another serial retest prowjob 5368 if err := c.Sync(); err != nil { 5369 t.Fatalf("sync failed: %v", err) 5370 } 5371 5372 // ensure we have the two retest prowjobs 5373 if err := c.prowJobClient.List(c.ctx, &pjs); err != nil { 5374 t.Fatalf("failed to list prowjobs: %v", err) 5375 } 5376 if n := len(pjs.Items); n != 2 { 5377 t.Errorf("expected to find exactly two prowjobs, got %d from list %+v", n, pjs) 5378 } 5379 5380 }