github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/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 "net/http" 25 "net/http/httptest" 26 "reflect" 27 "testing" 28 29 githubql "github.com/shurcooL/githubv4" 30 "github.com/sirupsen/logrus" 31 32 "k8s.io/apimachinery/pkg/util/sets" 33 "k8s.io/test-infra/prow/config" 34 "k8s.io/test-infra/prow/git/localgit" 35 "k8s.io/test-infra/prow/github" 36 "k8s.io/test-infra/prow/kube" 37 "k8s.io/test-infra/prow/tide/history" 38 ) 39 40 func testPullsMatchList(t *testing.T, test string, actual []PullRequest, expected []int) { 41 if len(actual) != len(expected) { 42 t.Errorf("Wrong size for case %s. Got PRs %+v, wanted numbers %v.", test, actual, expected) 43 return 44 } 45 for _, pr := range actual { 46 var found bool 47 n1 := int(pr.Number) 48 for _, n2 := range expected { 49 if n1 == n2 { 50 found = true 51 } 52 } 53 if !found { 54 t.Errorf("For case %s, found PR %d but shouldn't have.", test, n1) 55 } 56 } 57 } 58 59 func TestAccumulateBatch(t *testing.T) { 60 jobSet := sets.NewString("foo", "bar", "baz") 61 type pull struct { 62 number int 63 sha string 64 } 65 type prowjob struct { 66 prs []pull 67 job string 68 state kube.ProwJobState 69 } 70 tests := []struct { 71 name string 72 presubmits map[int]sets.String 73 pulls []pull 74 prowJobs []prowjob 75 76 merges []int 77 pending bool 78 }{ 79 { 80 name: "no batches running", 81 }, 82 { 83 name: "batch pending", 84 presubmits: map[int]sets.String{1: sets.NewString("foo"), 2: sets.NewString("foo")}, 85 pulls: []pull{{1, "a"}, {2, "b"}}, 86 prowJobs: []prowjob{{job: "foo", state: kube.PendingState, prs: []pull{{1, "a"}}}}, 87 pending: true, 88 }, 89 { 90 name: "pending batch missing presubmits is ignored", 91 presubmits: map[int]sets.String{1: jobSet}, 92 pulls: []pull{{1, "a"}, {2, "b"}}, 93 prowJobs: []prowjob{{job: "foo", state: kube.PendingState, prs: []pull{{1, "a"}}}}, 94 }, 95 { 96 name: "batch pending, successful previous run", 97 presubmits: map[int]sets.String{1: jobSet, 2: jobSet}, 98 pulls: []pull{{1, "a"}, {2, "b"}}, 99 prowJobs: []prowjob{ 100 {job: "foo", state: kube.PendingState, prs: []pull{{1, "a"}}}, 101 {job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}}}, 102 {job: "baz", state: kube.SuccessState, prs: []pull{{1, "a"}}}, 103 {job: "foo", state: kube.SuccessState, prs: []pull{{2, "b"}}}, 104 {job: "bar", state: kube.SuccessState, prs: []pull{{2, "b"}}}, 105 {job: "baz", state: kube.SuccessState, prs: []pull{{2, "b"}}}, 106 }, 107 pending: true, 108 merges: []int{2}, 109 }, 110 { 111 name: "successful run", 112 presubmits: map[int]sets.String{1: jobSet, 2: jobSet}, 113 pulls: []pull{{1, "a"}, {2, "b"}}, 114 prowJobs: []prowjob{ 115 {job: "foo", state: kube.SuccessState, prs: []pull{{2, "b"}}}, 116 {job: "bar", state: kube.SuccessState, prs: []pull{{2, "b"}}}, 117 {job: "baz", state: kube.SuccessState, prs: []pull{{2, "b"}}}, 118 }, 119 merges: []int{2}, 120 }, 121 { 122 name: "successful run, multiple PRs", 123 presubmits: map[int]sets.String{1: jobSet, 2: jobSet}, 124 pulls: []pull{{1, "a"}, {2, "b"}}, 125 prowJobs: []prowjob{ 126 {job: "foo", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 127 {job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 128 {job: "baz", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 129 }, 130 merges: []int{1, 2}, 131 }, 132 { 133 name: "successful run, failures in past", 134 presubmits: map[int]sets.String{1: jobSet, 2: jobSet}, 135 pulls: []pull{{1, "a"}, {2, "b"}}, 136 prowJobs: []prowjob{ 137 {job: "foo", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 138 {job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 139 {job: "baz", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 140 {job: "foo", state: kube.FailureState, prs: []pull{{1, "a"}, {2, "b"}}}, 141 {job: "baz", state: kube.FailureState, prs: []pull{{1, "a"}, {2, "b"}}}, 142 {job: "foo", state: kube.FailureState, prs: []pull{{1, "c"}, {2, "b"}}}, 143 }, 144 merges: []int{1, 2}, 145 }, 146 { 147 name: "failures", 148 presubmits: map[int]sets.String{1: jobSet, 2: jobSet}, 149 pulls: []pull{{1, "a"}, {2, "b"}}, 150 prowJobs: []prowjob{ 151 {job: "foo", state: kube.FailureState, prs: []pull{{1, "a"}, {2, "b"}}}, 152 {job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 153 {job: "baz", state: kube.FailureState, prs: []pull{{1, "a"}, {2, "b"}}}, 154 {job: "foo", state: kube.FailureState, prs: []pull{{1, "c"}, {2, "b"}}}, 155 }, 156 }, 157 { 158 name: "missing job required by one PR", 159 presubmits: map[int]sets.String{1: jobSet, 2: jobSet.Union(sets.NewString("boo"))}, 160 pulls: []pull{{1, "a"}, {2, "b"}}, 161 prowJobs: []prowjob{ 162 {job: "foo", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 163 {job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 164 {job: "baz", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 165 }, 166 }, 167 { 168 name: "successful run with PR that requires additional job", 169 presubmits: map[int]sets.String{1: jobSet, 2: jobSet.Union(sets.NewString("boo"))}, 170 pulls: []pull{{1, "a"}, {2, "b"}}, 171 prowJobs: []prowjob{ 172 {job: "foo", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 173 {job: "bar", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 174 {job: "baz", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 175 {job: "boo", state: kube.SuccessState, prs: []pull{{1, "a"}, {2, "b"}}}, 176 }, 177 merges: []int{1, 2}, 178 }, 179 { 180 name: "no presubmits", 181 pulls: []pull{{1, "a"}, {2, "b"}}, 182 pending: false, 183 }, 184 { 185 name: "pending batch with PR that left pool, successful previous run", 186 presubmits: map[int]sets.String{2: jobSet}, 187 pulls: []pull{{2, "b"}}, 188 prowJobs: []prowjob{ 189 {job: "foo", state: kube.PendingState, prs: []pull{{1, "a"}}}, 190 {job: "foo", state: kube.SuccessState, prs: []pull{{2, "b"}}}, 191 {job: "bar", state: kube.SuccessState, prs: []pull{{2, "b"}}}, 192 {job: "baz", state: kube.SuccessState, prs: []pull{{2, "b"}}}, 193 }, 194 pending: false, 195 merges: []int{2}, 196 }, 197 } 198 for _, test := range tests { 199 var pulls []PullRequest 200 for _, p := range test.pulls { 201 pr := PullRequest{ 202 Number: githubql.Int(p.number), 203 HeadRefOID: githubql.String(p.sha), 204 } 205 pulls = append(pulls, pr) 206 } 207 var pjs []kube.ProwJob 208 for _, pj := range test.prowJobs { 209 npj := kube.ProwJob{ 210 Spec: kube.ProwJobSpec{ 211 Job: pj.job, 212 Context: pj.job, 213 Type: kube.BatchJob, 214 Refs: new(kube.Refs), 215 }, 216 Status: kube.ProwJobStatus{State: pj.state}, 217 } 218 for _, pr := range pj.prs { 219 npj.Spec.Refs.Pulls = append(npj.Spec.Refs.Pulls, kube.Pull{ 220 Number: pr.number, 221 SHA: pr.sha, 222 }) 223 } 224 pjs = append(pjs, npj) 225 } 226 merges, pending := accumulateBatch(test.presubmits, pulls, pjs, logrus.NewEntry(logrus.New())) 227 if (len(pending) > 0) != test.pending { 228 t.Errorf("For case \"%s\", got wrong pending.", test.name) 229 } 230 testPullsMatchList(t, test.name, merges, test.merges) 231 } 232 } 233 234 func TestAccumulate(t *testing.T) { 235 jobSet := sets.NewString("job1", "job2") 236 type prowjob struct { 237 prNumber int 238 job string 239 state kube.ProwJobState 240 sha string 241 } 242 tests := []struct { 243 presubmits map[int]sets.String 244 pullRequests map[int]string 245 prowJobs []prowjob 246 247 successes []int 248 pendings []int 249 none []int 250 }{ 251 { 252 pullRequests: map[int]string{1: "", 2: "", 3: "", 4: "", 5: "", 6: "", 7: ""}, 253 presubmits: map[int]sets.String{ 254 1: jobSet, 255 2: jobSet, 256 3: jobSet, 257 4: jobSet, 258 5: jobSet, 259 6: jobSet, 260 7: jobSet, 261 }, 262 prowJobs: []prowjob{ 263 {2, "job1", kube.PendingState, ""}, 264 {3, "job1", kube.PendingState, ""}, 265 {3, "job2", kube.TriggeredState, ""}, 266 {4, "job1", kube.FailureState, ""}, 267 {4, "job2", kube.PendingState, ""}, 268 {5, "job1", kube.PendingState, ""}, 269 {5, "job2", kube.FailureState, ""}, 270 {5, "job2", kube.PendingState, ""}, 271 {6, "job1", kube.SuccessState, ""}, 272 {6, "job2", kube.PendingState, ""}, 273 {7, "job1", kube.SuccessState, ""}, 274 {7, "job2", kube.SuccessState, ""}, 275 {7, "job1", kube.FailureState, ""}, 276 }, 277 278 successes: []int{7}, 279 pendings: []int{3, 5, 6}, 280 none: []int{1, 2, 4}, 281 }, 282 { 283 pullRequests: map[int]string{7: ""}, 284 presubmits: map[int]sets.String{7: sets.NewString("job1", "job2", "job3", "job4")}, 285 prowJobs: []prowjob{ 286 {7, "job1", kube.SuccessState, ""}, 287 {7, "job2", kube.FailureState, ""}, 288 {7, "job3", kube.FailureState, ""}, 289 {7, "job4", kube.FailureState, ""}, 290 {7, "job3", kube.FailureState, ""}, 291 {7, "job4", kube.FailureState, ""}, 292 {7, "job2", kube.SuccessState, ""}, 293 {7, "job3", kube.SuccessState, ""}, 294 {7, "job4", kube.FailureState, ""}, 295 }, 296 297 successes: []int{}, 298 pendings: []int{}, 299 none: []int{7}, 300 }, 301 { 302 pullRequests: map[int]string{7: ""}, 303 presubmits: map[int]sets.String{7: sets.NewString("job1", "job2", "job3", "job4")}, 304 prowJobs: []prowjob{ 305 {7, "job1", kube.FailureState, ""}, 306 {7, "job2", kube.FailureState, ""}, 307 {7, "job3", kube.FailureState, ""}, 308 {7, "job4", kube.FailureState, ""}, 309 {7, "job3", kube.FailureState, ""}, 310 {7, "job4", kube.FailureState, ""}, 311 {7, "job2", kube.FailureState, ""}, 312 {7, "job3", kube.FailureState, ""}, 313 {7, "job4", kube.FailureState, ""}, 314 }, 315 316 successes: []int{}, 317 pendings: []int{}, 318 none: []int{7}, 319 }, 320 { 321 pullRequests: map[int]string{7: ""}, 322 presubmits: map[int]sets.String{7: sets.NewString("job1", "job2", "job3", "job4")}, 323 prowJobs: []prowjob{ 324 {7, "job1", kube.SuccessState, ""}, 325 {7, "job2", kube.FailureState, ""}, 326 {7, "job3", kube.FailureState, ""}, 327 {7, "job4", kube.FailureState, ""}, 328 {7, "job3", kube.FailureState, ""}, 329 {7, "job4", kube.FailureState, ""}, 330 {7, "job2", kube.SuccessState, ""}, 331 {7, "job3", kube.SuccessState, ""}, 332 {7, "job4", kube.SuccessState, ""}, 333 {7, "job1", kube.FailureState, ""}, 334 }, 335 336 successes: []int{7}, 337 pendings: []int{}, 338 none: []int{}, 339 }, 340 { 341 pullRequests: map[int]string{7: ""}, 342 presubmits: map[int]sets.String{7: sets.NewString("job1", "job2", "job3", "job4")}, 343 prowJobs: []prowjob{ 344 {7, "job1", kube.SuccessState, ""}, 345 {7, "job2", kube.FailureState, ""}, 346 {7, "job3", kube.FailureState, ""}, 347 {7, "job4", kube.FailureState, ""}, 348 {7, "job3", kube.FailureState, ""}, 349 {7, "job4", kube.FailureState, ""}, 350 {7, "job2", kube.SuccessState, ""}, 351 {7, "job3", kube.SuccessState, ""}, 352 {7, "job4", kube.PendingState, ""}, 353 {7, "job1", kube.FailureState, ""}, 354 }, 355 356 successes: []int{}, 357 pendings: []int{7}, 358 none: []int{}, 359 }, 360 { 361 presubmits: map[int]sets.String{7: sets.NewString("job1")}, 362 pullRequests: map[int]string{7: "new", 8: "new"}, 363 prowJobs: []prowjob{ 364 {7, "job1", kube.SuccessState, "old"}, 365 {7, "job1", kube.FailureState, "new"}, 366 {8, "job1", kube.FailureState, "old"}, 367 {8, "job1", kube.SuccessState, "new"}, 368 }, 369 370 successes: []int{8}, 371 pendings: []int{}, 372 none: []int{7}, 373 }, 374 { 375 pullRequests: map[int]string{7: "new", 8: "new"}, 376 prowJobs: []prowjob{}, 377 378 successes: []int{8, 7}, 379 pendings: []int{}, 380 none: []int{}, 381 }, 382 } 383 384 for i, test := range tests { 385 var pulls []PullRequest 386 for num, sha := range test.pullRequests { 387 pulls = append( 388 pulls, 389 PullRequest{Number: githubql.Int(num), HeadRefOID: githubql.String(sha)}, 390 ) 391 } 392 var pjs []kube.ProwJob 393 for _, pj := range test.prowJobs { 394 pjs = append(pjs, kube.ProwJob{ 395 Spec: kube.ProwJobSpec{ 396 Job: pj.job, 397 Context: pj.job, 398 Type: kube.PresubmitJob, 399 Refs: &kube.Refs{Pulls: []kube.Pull{{Number: pj.prNumber, SHA: pj.sha}}}, 400 }, 401 Status: kube.ProwJobStatus{State: pj.state}, 402 }) 403 } 404 405 successes, pendings, nones := accumulate(test.presubmits, pulls, pjs, logrus.NewEntry(logrus.New())) 406 407 t.Logf("test run %d", i) 408 testPullsMatchList(t, "successes", successes, test.successes) 409 testPullsMatchList(t, "pendings", pendings, test.pendings) 410 testPullsMatchList(t, "nones", nones, test.none) 411 } 412 } 413 414 type fgc struct { 415 prs []PullRequest 416 refs map[string]string 417 merged int 418 setStatus bool 419 420 expectedSHA string 421 combinedStatus map[string]string 422 } 423 424 func (f *fgc) GetRef(o, r, ref string) (string, error) { 425 return f.refs[o+"/"+r+" "+ref], nil 426 } 427 428 func (f *fgc) Query(ctx context.Context, q interface{}, vars map[string]interface{}) error { 429 sq, ok := q.(*searchQuery) 430 if !ok { 431 return errors.New("unexpected query type") 432 } 433 for _, pr := range f.prs { 434 sq.Search.Nodes = append( 435 sq.Search.Nodes, 436 struct { 437 PullRequest PullRequest `graphql:"... on PullRequest"` 438 }{PullRequest: pr}, 439 ) 440 } 441 return nil 442 } 443 444 func (f *fgc) Merge(org, repo string, number int, details github.MergeDetails) error { 445 if details.SHA == "uh oh" { 446 return errors.New("invalid sha") 447 } 448 f.merged++ 449 return nil 450 } 451 452 func (f *fgc) CreateStatus(org, repo, ref string, s github.Status) error { 453 switch s.State { 454 case github.StatusSuccess, github.StatusError, github.StatusPending, github.StatusFailure: 455 f.setStatus = true 456 return nil 457 } 458 return fmt.Errorf("invalid 'state' value: %q", s.State) 459 } 460 461 func (f *fgc) GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error) { 462 if f.expectedSHA != ref { 463 return nil, errors.New("bad combined status request: incorrect sha") 464 } 465 var statuses []github.Status 466 for c, s := range f.combinedStatus { 467 statuses = append(statuses, github.Status{Context: c, State: s}) 468 } 469 return &github.CombinedStatus{ 470 Statuses: statuses, 471 }, 472 nil 473 } 474 475 func (f *fgc) GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) { 476 if number != 100 { 477 return nil, nil 478 } 479 return []github.PullRequestChange{ 480 { 481 Filename: "CHANGED", 482 }, 483 }, 484 nil 485 } 486 487 // TestDividePool ensures that subpools returned by dividePool satisfy a few 488 // important invariants. 489 func TestDividePool(t *testing.T) { 490 testPulls := []struct { 491 org string 492 repo string 493 number int 494 branch string 495 }{ 496 { 497 org: "k", 498 repo: "t-i", 499 number: 5, 500 branch: "master", 501 }, 502 { 503 org: "k", 504 repo: "t-i", 505 number: 6, 506 branch: "master", 507 }, 508 { 509 org: "k", 510 repo: "k", 511 number: 123, 512 branch: "master", 513 }, 514 { 515 org: "k", 516 repo: "k", 517 number: 1000, 518 branch: "release-1.6", 519 }, 520 } 521 testPJs := []struct { 522 jobType kube.ProwJobType 523 org string 524 repo string 525 baseRef string 526 baseSHA string 527 }{ 528 { 529 jobType: kube.PresubmitJob, 530 org: "k", 531 repo: "t-i", 532 baseRef: "master", 533 baseSHA: "123", 534 }, 535 { 536 jobType: kube.BatchJob, 537 org: "k", 538 repo: "t-i", 539 baseRef: "master", 540 baseSHA: "123", 541 }, 542 { 543 jobType: kube.PeriodicJob, 544 }, 545 { 546 jobType: kube.PresubmitJob, 547 org: "k", 548 repo: "t-i", 549 baseRef: "patch", 550 baseSHA: "123", 551 }, 552 { 553 jobType: kube.PresubmitJob, 554 org: "k", 555 repo: "t-i", 556 baseRef: "master", 557 baseSHA: "abc", 558 }, 559 { 560 jobType: kube.PresubmitJob, 561 org: "o", 562 repo: "t-i", 563 baseRef: "master", 564 baseSHA: "123", 565 }, 566 { 567 jobType: kube.PresubmitJob, 568 org: "k", 569 repo: "other", 570 baseRef: "master", 571 baseSHA: "123", 572 }, 573 } 574 fc := &fgc{ 575 refs: map[string]string{"k/t-i heads/master": "123"}, 576 } 577 c := &Controller{ 578 ghc: fc, 579 logger: logrus.WithField("component", "tide"), 580 } 581 pulls := make(map[string]PullRequest) 582 for _, p := range testPulls { 583 npr := PullRequest{Number: githubql.Int(p.number)} 584 npr.BaseRef.Name = githubql.String(p.branch) 585 npr.BaseRef.Prefix = "refs/heads/" 586 npr.Repository.Name = githubql.String(p.repo) 587 npr.Repository.Owner.Login = githubql.String(p.org) 588 pulls[prKey(&npr)] = npr 589 } 590 var pjs []kube.ProwJob 591 for _, pj := range testPJs { 592 pjs = append(pjs, kube.ProwJob{ 593 Spec: kube.ProwJobSpec{ 594 Type: pj.jobType, 595 Refs: &kube.Refs{ 596 Org: pj.org, 597 Repo: pj.repo, 598 BaseRef: pj.baseRef, 599 BaseSHA: pj.baseSHA, 600 }, 601 }, 602 }) 603 } 604 sps, err := c.dividePool(pulls, pjs) 605 if err != nil { 606 t.Fatalf("Error dividing pool: %v", err) 607 } 608 if len(sps) == 0 { 609 t.Error("No subpools.") 610 } 611 for _, sp := range sps { 612 name := fmt.Sprintf("%s/%s %s", sp.org, sp.repo, sp.branch) 613 sha := fc.refs[sp.org+"/"+sp.repo+" heads/"+sp.branch] 614 if sp.sha != sha { 615 t.Errorf("For subpool %s, got sha %s, expected %s.", name, sp.sha, sha) 616 } 617 if len(sp.prs) == 0 { 618 t.Errorf("Subpool %s has no PRs.", name) 619 } 620 for _, pr := range sp.prs { 621 if string(pr.Repository.Owner.Login) != sp.org || string(pr.Repository.Name) != sp.repo || string(pr.BaseRef.Name) != sp.branch { 622 t.Errorf("PR in wrong subpool. Got PR %+v in subpool %s.", pr, name) 623 } 624 } 625 for _, pj := range sp.pjs { 626 if pj.Spec.Type != kube.PresubmitJob && pj.Spec.Type != kube.BatchJob { 627 t.Errorf("PJ with bad type in subpool %s: %+v", name, pj) 628 } 629 if pj.Spec.Refs.Org != sp.org || pj.Spec.Refs.Repo != sp.repo || pj.Spec.Refs.BaseRef != sp.branch || pj.Spec.Refs.BaseSHA != sp.sha { 630 t.Errorf("PJ in wrong subpool. Got PJ %+v in subpool %s.", pj, name) 631 } 632 } 633 } 634 } 635 636 func TestPickBatch(t *testing.T) { 637 lg, gc, err := localgit.New() 638 if err != nil { 639 t.Fatalf("Error making local git: %v", err) 640 } 641 defer gc.Clean() 642 defer lg.Clean() 643 if err := lg.MakeFakeRepo("o", "r"); err != nil { 644 t.Fatalf("Error making fake repo: %v", err) 645 } 646 if err := lg.AddCommit("o", "r", map[string][]byte{"foo": []byte("foo")}); err != nil { 647 t.Fatalf("Adding initial commit: %v", err) 648 } 649 testprs := []struct { 650 files map[string][]byte 651 success bool 652 number int 653 654 included bool 655 }{ 656 { 657 files: map[string][]byte{"bar": []byte("ok")}, 658 success: true, 659 number: 0, 660 included: true, 661 }, 662 { 663 files: map[string][]byte{"foo": []byte("ok")}, 664 success: true, 665 number: 1, 666 included: true, 667 }, 668 { 669 files: map[string][]byte{"bar": []byte("conflicts with 0")}, 670 success: true, 671 number: 2, 672 included: false, 673 }, 674 { 675 files: map[string][]byte{"qux": []byte("ok")}, 676 success: false, 677 number: 6, 678 included: false, 679 }, 680 { 681 files: map[string][]byte{"bazel": []byte("ok")}, 682 success: true, 683 number: 7, 684 included: false, // batch of 5 smallest excludes this 685 }, 686 { 687 files: map[string][]byte{"other": []byte("ok")}, 688 success: true, 689 number: 5, 690 included: true, 691 }, 692 { 693 files: map[string][]byte{"changes": []byte("ok")}, 694 success: true, 695 number: 4, 696 included: true, 697 }, 698 { 699 files: map[string][]byte{"something": []byte("ok")}, 700 success: true, 701 number: 3, 702 included: true, 703 }, 704 } 705 sp := subpool{ 706 log: logrus.WithField("component", "tide"), 707 org: "o", 708 repo: "r", 709 branch: "master", 710 sha: "master", 711 } 712 for _, testpr := range testprs { 713 if err := lg.CheckoutNewBranch("o", "r", fmt.Sprintf("pr-%d", testpr.number)); err != nil { 714 t.Fatalf("Error checking out new branch: %v", err) 715 } 716 if err := lg.AddCommit("o", "r", testpr.files); err != nil { 717 t.Fatalf("Error adding commit: %v", err) 718 } 719 if err := lg.Checkout("o", "r", "master"); err != nil { 720 t.Fatalf("Error checking out master: %v", err) 721 } 722 oid := githubql.String(fmt.Sprintf("origin/pr-%d", testpr.number)) 723 var pr PullRequest 724 pr.Number = githubql.Int(testpr.number) 725 pr.HeadRefOID = oid 726 pr.Commits.Nodes = []struct { 727 Commit Commit 728 }{{Commit: Commit{OID: oid}}} 729 pr.Commits.Nodes[0].Commit.Status.Contexts = append(pr.Commits.Nodes[0].Commit.Status.Contexts, Context{State: githubql.StatusStateSuccess}) 730 if !testpr.success { 731 pr.Commits.Nodes[0].Commit.Status.Contexts[0].State = githubql.StatusStateFailure 732 } 733 sp.prs = append(sp.prs, pr) 734 } 735 ca := &config.Agent{} 736 ca.Set(&config.Config{}) 737 c := &Controller{ 738 logger: logrus.WithField("component", "tide"), 739 gc: gc, 740 ca: ca, 741 } 742 prs, err := c.pickBatch(sp, &config.TideContextPolicy{}) 743 if err != nil { 744 t.Fatalf("Error from pickBatch: %v", err) 745 } 746 for _, testpr := range testprs { 747 var found bool 748 for _, pr := range prs { 749 if int(pr.Number) == testpr.number { 750 found = true 751 break 752 } 753 } 754 if found && !testpr.included { 755 t.Errorf("PR %d should not be picked.", testpr.number) 756 } else if !found && testpr.included { 757 t.Errorf("PR %d should be picked.", testpr.number) 758 } 759 } 760 } 761 762 type fkc struct { 763 createdJobs []kube.ProwJob 764 } 765 766 func (c *fkc) ListProwJobs(string) ([]kube.ProwJob, error) { 767 return nil, nil 768 } 769 770 func (c *fkc) CreateProwJob(pj kube.ProwJob) (kube.ProwJob, error) { 771 c.createdJobs = append(c.createdJobs, pj) 772 return pj, nil 773 } 774 775 func TestTakeAction(t *testing.T) { 776 // PRs 0-9 exist. All are mergable, and all are passing tests. 777 testcases := []struct { 778 name string 779 780 batchPending bool 781 successes []int 782 pendings []int 783 nones []int 784 batchMerges []int 785 presubmits map[int]sets.String 786 787 merged int 788 triggered int 789 triggeredBatches int 790 action Action 791 }{ 792 { 793 name: "no prs to test, should do nothing", 794 795 batchPending: true, 796 successes: []int{}, 797 pendings: []int{}, 798 nones: []int{}, 799 batchMerges: []int{}, 800 presubmits: map[int]sets.String{100: sets.NewString("foo", "if-changed")}, 801 802 merged: 0, 803 triggered: 0, 804 action: Wait, 805 }, 806 { 807 name: "pending batch, pending serial, nothing to do", 808 809 batchPending: true, 810 successes: []int{}, 811 pendings: []int{1}, 812 nones: []int{0, 2}, 813 batchMerges: []int{}, 814 presubmits: map[int]sets.String{100: sets.NewString("foo", "if-changed")}, 815 816 merged: 0, 817 triggered: 0, 818 action: Wait, 819 }, 820 { 821 name: "pending batch, successful serial, nothing to do", 822 823 batchPending: true, 824 successes: []int{1}, 825 pendings: []int{}, 826 nones: []int{0, 2}, 827 batchMerges: []int{}, 828 presubmits: map[int]sets.String{100: sets.NewString("foo", "if-changed")}, 829 830 merged: 0, 831 triggered: 0, 832 action: Wait, 833 }, 834 { 835 name: "pending batch, should trigger serial", 836 837 batchPending: true, 838 successes: []int{}, 839 pendings: []int{}, 840 nones: []int{0, 1, 2}, 841 batchMerges: []int{}, 842 presubmits: map[int]sets.String{100: sets.NewString("foo", "if-changed")}, 843 844 merged: 0, 845 triggered: 1, 846 action: Trigger, 847 }, 848 { 849 name: "no pending batch, should trigger batch", 850 851 batchPending: false, 852 successes: []int{}, 853 pendings: []int{0}, 854 nones: []int{1, 2, 3}, 855 batchMerges: []int{}, 856 presubmits: map[int]sets.String{100: sets.NewString("foo", "if-changed")}, 857 858 merged: 0, 859 triggered: 1, 860 triggeredBatches: 1, 861 action: TriggerBatch, 862 }, 863 { 864 name: "one PR, should not trigger batch", 865 866 batchPending: false, 867 successes: []int{}, 868 pendings: []int{}, 869 nones: []int{0}, 870 batchMerges: []int{}, 871 presubmits: map[int]sets.String{100: sets.NewString("foo", "if-changed")}, 872 873 merged: 0, 874 triggered: 1, 875 action: Trigger, 876 }, 877 { 878 name: "successful PR, should merge", 879 880 batchPending: false, 881 successes: []int{0}, 882 pendings: []int{}, 883 nones: []int{1, 2, 3}, 884 batchMerges: []int{}, 885 presubmits: map[int]sets.String{100: sets.NewString("foo", "if-changed")}, 886 887 merged: 1, 888 triggered: 0, 889 action: Merge, 890 }, 891 { 892 name: "successful batch, should merge", 893 894 batchPending: false, 895 successes: []int{0, 1}, 896 pendings: []int{2, 3}, 897 nones: []int{4, 5}, 898 batchMerges: []int{6, 7, 8}, 899 presubmits: map[int]sets.String{100: sets.NewString("foo", "if-changed")}, 900 901 merged: 3, 902 triggered: 0, 903 action: MergeBatch, 904 }, 905 { 906 name: "one PR that triggers RunIfChangedJob", 907 908 batchPending: false, 909 successes: []int{}, 910 pendings: []int{}, 911 nones: []int{100}, 912 batchMerges: []int{}, 913 presubmits: map[int]sets.String{100: sets.NewString("foo", "if-changed")}, 914 915 merged: 0, 916 triggered: 2, 917 action: Trigger, 918 }, 919 { 920 name: "no presubmits, merge", 921 922 batchPending: false, 923 successes: []int{5, 4}, 924 pendings: []int{}, 925 nones: []int{}, 926 batchMerges: []int{}, 927 928 merged: 1, 929 triggered: 0, 930 action: Merge, 931 }, 932 { 933 name: "no presubmits, wait", 934 935 batchPending: false, 936 successes: []int{}, 937 pendings: []int{}, 938 nones: []int{}, 939 batchMerges: []int{}, 940 941 merged: 0, 942 triggered: 0, 943 action: Wait, 944 }, 945 } 946 947 for _, tc := range testcases { 948 ca := &config.Agent{} 949 cfg := &config.Config{} 950 if err := cfg.SetPresubmits( 951 map[string][]config.Presubmit{ 952 "o/r": { 953 { 954 Context: "foo", 955 Trigger: "/test all", 956 RerunCommand: "/test all", 957 AlwaysRun: true, 958 }, 959 { 960 Context: "if-changed", 961 Trigger: "/test if-changed", 962 RerunCommand: "/test if-changed", 963 RegexpChangeMatcher: config.RegexpChangeMatcher{ 964 RunIfChanged: "CHANGED", 965 }, 966 }, 967 }, 968 }, 969 ); err != nil { 970 t.Fatalf("failed to set presubmits: %v", err) 971 } 972 ca.Set(cfg) 973 if len(tc.presubmits) > 0 { 974 for i := 0; i <= 8; i++ { 975 tc.presubmits[i] = sets.NewString("foo") 976 } 977 } 978 lg, gc, err := localgit.New() 979 if err != nil { 980 t.Fatalf("Error making local git: %v", err) 981 } 982 defer gc.Clean() 983 defer lg.Clean() 984 if err := lg.MakeFakeRepo("o", "r"); err != nil { 985 t.Fatalf("Error making fake repo: %v", err) 986 } 987 if err := lg.AddCommit("o", "r", map[string][]byte{"foo": []byte("foo")}); err != nil { 988 t.Fatalf("Adding initial commit: %v", err) 989 } 990 991 sp := subpool{ 992 log: logrus.WithField("component", "tide"), 993 presubmitContexts: tc.presubmits, 994 cc: &config.TideContextPolicy{}, 995 org: "o", 996 repo: "r", 997 branch: "master", 998 sha: "master", 999 } 1000 genPulls := func(nums []int) []PullRequest { 1001 var prs []PullRequest 1002 for _, i := range nums { 1003 if err := lg.CheckoutNewBranch("o", "r", fmt.Sprintf("pr-%d", i)); err != nil { 1004 t.Fatalf("Error checking out new branch: %v", err) 1005 } 1006 if err := lg.AddCommit("o", "r", map[string][]byte{fmt.Sprintf("%d", i): []byte("WOW")}); err != nil { 1007 t.Fatalf("Error adding commit: %v", err) 1008 } 1009 if err := lg.Checkout("o", "r", "master"); err != nil { 1010 t.Fatalf("Error checking out master: %v", err) 1011 } 1012 oid := githubql.String(fmt.Sprintf("origin/pr-%d", i)) 1013 var pr PullRequest 1014 pr.Number = githubql.Int(i) 1015 pr.HeadRefOID = oid 1016 pr.Commits.Nodes = []struct { 1017 Commit Commit 1018 }{{Commit: Commit{OID: oid}}} 1019 sp.prs = append(sp.prs, pr) 1020 prs = append(prs, pr) 1021 } 1022 return prs 1023 } 1024 var fkc fkc 1025 var fgc fgc 1026 c := &Controller{ 1027 logger: logrus.WithField("controller", "tide"), 1028 gc: gc, 1029 ca: ca, 1030 ghc: &fgc, 1031 kc: &fkc, 1032 } 1033 var batchPending []PullRequest 1034 if tc.batchPending { 1035 batchPending = []PullRequest{{}} 1036 } 1037 t.Logf("Test case: %s", tc.name) 1038 if act, _, err := c.takeAction(sp, batchPending, genPulls(tc.successes), genPulls(tc.pendings), genPulls(tc.nones), genPulls(tc.batchMerges)); err != nil { 1039 t.Errorf("Error in takeAction: %v", err) 1040 continue 1041 } else if act != tc.action { 1042 t.Errorf("Wrong action. Got %v, wanted %v.", act, tc.action) 1043 } 1044 if tc.triggered != len(fkc.createdJobs) { 1045 t.Errorf("Wrong number of jobs triggered. Got %d, expected %d.", len(fkc.createdJobs), tc.triggered) 1046 } 1047 if tc.merged != fgc.merged { 1048 t.Errorf("Wrong number of merges. Got %d, expected %d.", fgc.merged, tc.merged) 1049 } 1050 // Ensure that the correct number of batch jobs were triggered 1051 batches := 0 1052 for _, job := range fkc.createdJobs { 1053 if (len(job.Spec.Refs.Pulls) > 1) != (job.Spec.Type == kube.BatchJob) { 1054 t.Error("Found a batch job that doesn't contain multiple pull refs!") 1055 } 1056 if len(job.Spec.Refs.Pulls) > 1 { 1057 batches++ 1058 } 1059 } 1060 if tc.triggeredBatches != batches { 1061 t.Errorf("Wrong number of batches triggered. Got %d, expected %d.", batches, tc.triggeredBatches) 1062 } 1063 } 1064 } 1065 1066 func TestServeHTTP(t *testing.T) { 1067 pr1 := PullRequest{} 1068 pr1.Commits.Nodes = append(pr1.Commits.Nodes, struct{ Commit Commit }{}) 1069 pr1.Commits.Nodes[0].Commit.Status.Contexts = []Context{{ 1070 Context: githubql.String("coverage/coveralls"), 1071 Description: githubql.String("Coverage increased (+0.1%) to 27.599%"), 1072 }} 1073 c := &Controller{ 1074 pools: []Pool{ 1075 { 1076 MissingPRs: []PullRequest{pr1}, 1077 Action: Merge, 1078 }, 1079 }, 1080 History: history.New(100), 1081 } 1082 s := httptest.NewServer(c) 1083 defer s.Close() 1084 resp, err := http.Get(s.URL) 1085 if err != nil { 1086 t.Errorf("GET error: %v", err) 1087 } 1088 defer resp.Body.Close() 1089 var pools []Pool 1090 if err := json.NewDecoder(resp.Body).Decode(&pools); err != nil { 1091 t.Fatalf("JSON decoding error: %v", err) 1092 } 1093 if !reflect.DeepEqual(c.pools, pools) { 1094 t.Errorf("Received pools %v do not match original pools %v.", pools, c.pools) 1095 } 1096 } 1097 1098 func TestHeadContexts(t *testing.T) { 1099 type commitContext struct { 1100 // one context per commit for testing 1101 context string 1102 sha string 1103 } 1104 1105 win := "win" 1106 lose := "lose" 1107 headSHA := "head" 1108 testCases := []struct { 1109 name string 1110 commitContexts []commitContext 1111 expectAPICall bool 1112 }{ 1113 { 1114 name: "first commit is head", 1115 commitContexts: []commitContext{ 1116 {context: win, sha: headSHA}, 1117 {context: lose, sha: "other"}, 1118 {context: lose, sha: "sha"}, 1119 }, 1120 }, 1121 { 1122 name: "last commit is head", 1123 commitContexts: []commitContext{ 1124 {context: lose, sha: "shaaa"}, 1125 {context: lose, sha: "other"}, 1126 {context: win, sha: headSHA}, 1127 }, 1128 }, 1129 { 1130 name: "no commit is head", 1131 commitContexts: []commitContext{ 1132 {context: lose, sha: "shaaa"}, 1133 {context: lose, sha: "other"}, 1134 {context: lose, sha: "sha"}, 1135 }, 1136 expectAPICall: true, 1137 }, 1138 } 1139 1140 for _, tc := range testCases { 1141 t.Logf("Running test case %q", tc.name) 1142 fgc := &fgc{combinedStatus: map[string]string{win: string(githubql.StatusStateSuccess)}} 1143 if tc.expectAPICall { 1144 fgc.expectedSHA = headSHA 1145 } 1146 pr := &PullRequest{HeadRefOID: githubql.String(headSHA)} 1147 for _, ctx := range tc.commitContexts { 1148 commit := Commit{ 1149 Status: struct{ Contexts []Context }{ 1150 Contexts: []Context{ 1151 { 1152 Context: githubql.String(ctx.context), 1153 }, 1154 }, 1155 }, 1156 OID: githubql.String(ctx.sha), 1157 } 1158 pr.Commits.Nodes = append(pr.Commits.Nodes, struct{ Commit Commit }{commit}) 1159 } 1160 1161 contexts, err := headContexts(logrus.WithField("component", "tide"), fgc, pr) 1162 if err != nil { 1163 t.Fatalf("Unexpected error from headContexts: %v", err) 1164 } 1165 if len(contexts) != 1 || string(contexts[0].Context) != win { 1166 t.Errorf("Expected exactly 1 %q context, but got: %#v", win, contexts) 1167 } 1168 } 1169 } 1170 1171 func testPR(org, repo, branch string, number int, mergeable githubql.MergeableState) PullRequest { 1172 pr := PullRequest{ 1173 Number: githubql.Int(number), 1174 Mergeable: mergeable, 1175 HeadRefOID: githubql.String("SHA"), 1176 } 1177 pr.Repository.Owner.Login = githubql.String(org) 1178 pr.Repository.Name = githubql.String(repo) 1179 pr.Repository.NameWithOwner = githubql.String(fmt.Sprintf("%s/%s", org, repo)) 1180 pr.BaseRef.Name = githubql.String(branch) 1181 1182 pr.Commits.Nodes = append(pr.Commits.Nodes, struct{ Commit Commit }{ 1183 Commit{ 1184 Status: struct{ Contexts []Context }{ 1185 Contexts: []Context{ 1186 { 1187 Context: githubql.String("context"), 1188 State: githubql.StatusStateSuccess, 1189 }, 1190 }, 1191 }, 1192 OID: githubql.String("SHA"), 1193 }, 1194 }) 1195 return pr 1196 } 1197 1198 func TestSync(t *testing.T) { 1199 mergeableA := testPR("org", "repo", "A", 5, githubql.MergeableStateMergeable) 1200 unmergeableA := testPR("org", "repo", "A", 6, githubql.MergeableStateConflicting) 1201 unmergeableB := testPR("org", "repo", "B", 7, githubql.MergeableStateConflicting) 1202 unknownA := testPR("org", "repo", "A", 8, githubql.MergeableStateUnknown) 1203 1204 testcases := []struct { 1205 name string 1206 prs []PullRequest 1207 1208 expectedPools []Pool 1209 }{ 1210 { 1211 name: "no PRs", 1212 prs: []PullRequest{}, 1213 expectedPools: []Pool{}, 1214 }, 1215 { 1216 name: "1 mergeable PR", 1217 prs: []PullRequest{mergeableA}, 1218 expectedPools: []Pool{{ 1219 Org: "org", 1220 Repo: "repo", 1221 Branch: "A", 1222 SuccessPRs: []PullRequest{mergeableA}, 1223 Action: Merge, 1224 Target: []PullRequest{mergeableA}, 1225 }}, 1226 }, 1227 { 1228 name: "1 unmergeable PR", 1229 prs: []PullRequest{unmergeableA}, 1230 expectedPools: []Pool{}, 1231 }, 1232 { 1233 name: "1 unknown PR", 1234 prs: []PullRequest{unknownA}, 1235 expectedPools: []Pool{{ 1236 Org: "org", 1237 Repo: "repo", 1238 Branch: "A", 1239 SuccessPRs: []PullRequest{unknownA}, 1240 Action: Merge, 1241 Target: []PullRequest{unknownA}, 1242 }}, 1243 }, 1244 { 1245 name: "1 mergeable, 1 unmergeable (different pools)", 1246 prs: []PullRequest{mergeableA, unmergeableB}, 1247 expectedPools: []Pool{{ 1248 Org: "org", 1249 Repo: "repo", 1250 Branch: "A", 1251 SuccessPRs: []PullRequest{mergeableA}, 1252 Action: Merge, 1253 Target: []PullRequest{mergeableA}, 1254 }}, 1255 }, 1256 { 1257 name: "1 mergeable, 1 unmergeable (same pool)", 1258 prs: []PullRequest{mergeableA, unmergeableA}, 1259 expectedPools: []Pool{{ 1260 Org: "org", 1261 Repo: "repo", 1262 Branch: "A", 1263 SuccessPRs: []PullRequest{mergeableA}, 1264 Action: Merge, 1265 Target: []PullRequest{mergeableA}, 1266 }}, 1267 }, 1268 { 1269 name: "1 mergeable PR (satisfies multiple queries)", 1270 prs: []PullRequest{mergeableA, mergeableA}, 1271 expectedPools: []Pool{{ 1272 Org: "org", 1273 Repo: "repo", 1274 Branch: "A", 1275 SuccessPRs: []PullRequest{mergeableA}, 1276 Action: Merge, 1277 Target: []PullRequest{mergeableA}, 1278 }}, 1279 }, 1280 } 1281 1282 for _, tc := range testcases { 1283 t.Logf("Starting case %q...", tc.name) 1284 fgc := &fgc{prs: tc.prs} 1285 fkc := &fkc{} 1286 ca := &config.Agent{} 1287 ca.Set(&config.Config{ 1288 ProwConfig: config.ProwConfig{ 1289 Tide: config.Tide{ 1290 Queries: []config.TideQuery{{}}, 1291 MaxGoroutines: 4, 1292 }, 1293 }, 1294 }) 1295 sc := &statusController{ 1296 logger: logrus.WithField("controller", "status-update"), 1297 ghc: fgc, 1298 ca: ca, 1299 newPoolPending: make(chan bool, 1), 1300 shutDown: make(chan bool), 1301 } 1302 go sc.run() 1303 defer sc.shutdown() 1304 c := &Controller{ 1305 ca: ca, 1306 ghc: fgc, 1307 kc: fkc, 1308 logger: logrus.WithField("controller", "sync"), 1309 sc: sc, 1310 changedFiles: &changedFilesAgent{ 1311 ghc: fgc, 1312 nextChangeCache: make(map[changeCacheKey][]string), 1313 }, 1314 History: history.New(100), 1315 } 1316 1317 if err := c.Sync(); err != nil { 1318 t.Errorf("Unexpected error from 'Sync()': %v.", err) 1319 continue 1320 } 1321 if len(tc.expectedPools) != len(c.pools) { 1322 t.Errorf("Tide pools did not match expected. Got %#v, expected %#v.", c.pools, tc.expectedPools) 1323 continue 1324 } 1325 for _, expected := range tc.expectedPools { 1326 var match *Pool 1327 for i, actual := range c.pools { 1328 if expected.Org == actual.Org && expected.Repo == actual.Repo && expected.Branch == actual.Branch { 1329 match = &c.pools[i] 1330 } 1331 } 1332 if match == nil { 1333 t.Errorf("Failed to find expected pool %s/%s %s.", expected.Org, expected.Repo, expected.Branch) 1334 } else if !reflect.DeepEqual(*match, expected) { 1335 t.Errorf("Expected pool %#v does not match actual pool %#v.", expected, *match) 1336 } 1337 } 1338 } 1339 } 1340 1341 func TestFilterSubpool(t *testing.T) { 1342 presubmits := map[int]sets.String{ 1343 1: sets.NewString("pj-a"), 1344 2: sets.NewString("pj-a", "pj-b"), 1345 } 1346 1347 trueVar := true 1348 cc := &config.TideContextPolicy{ 1349 RequiredContexts: []string{"pj-a", "pj-b", "other-a"}, 1350 OptionalContexts: []string{"tide", "pj-c"}, 1351 SkipUnknownContexts: &trueVar, 1352 } 1353 1354 type pr struct { 1355 number int 1356 mergeable bool 1357 contexts []Context 1358 } 1359 tcs := []struct { 1360 name string 1361 1362 prs []pr 1363 expectedPRs []int // Empty indicates no subpool should be returned. 1364 }{ 1365 { 1366 name: "one mergeable passing PR (omitting optional context)", 1367 prs: []pr{ 1368 { 1369 number: 1, 1370 mergeable: true, 1371 contexts: []Context{ 1372 { 1373 Context: githubql.String("pj-a"), 1374 State: githubql.StatusStateSuccess, 1375 }, 1376 { 1377 Context: githubql.String("pj-b"), 1378 State: githubql.StatusStateSuccess, 1379 }, 1380 { 1381 Context: githubql.String("other-a"), 1382 State: githubql.StatusStateSuccess, 1383 }, 1384 }, 1385 }, 1386 }, 1387 expectedPRs: []int{1}, 1388 }, 1389 { 1390 name: "one unmergeable passing PR", 1391 prs: []pr{ 1392 { 1393 number: 1, 1394 mergeable: false, 1395 contexts: []Context{ 1396 { 1397 Context: githubql.String("pj-a"), 1398 State: githubql.StatusStateSuccess, 1399 }, 1400 { 1401 Context: githubql.String("pj-b"), 1402 State: githubql.StatusStateSuccess, 1403 }, 1404 { 1405 Context: githubql.String("other-a"), 1406 State: githubql.StatusStateSuccess, 1407 }, 1408 }, 1409 }, 1410 }, 1411 expectedPRs: []int{}, 1412 }, 1413 { 1414 name: "one mergeable PR pending non-PJ context (consider failing)", 1415 prs: []pr{ 1416 { 1417 number: 2, 1418 mergeable: true, 1419 contexts: []Context{ 1420 { 1421 Context: githubql.String("pj-a"), 1422 State: githubql.StatusStateSuccess, 1423 }, 1424 { 1425 Context: githubql.String("pj-b"), 1426 State: githubql.StatusStateSuccess, 1427 }, 1428 { 1429 Context: githubql.String("other-a"), 1430 State: githubql.StatusStatePending, 1431 }, 1432 }, 1433 }, 1434 }, 1435 expectedPRs: []int{}, 1436 }, 1437 { 1438 name: "one mergeable PR pending PJ context (consider in pool)", 1439 prs: []pr{ 1440 { 1441 number: 2, 1442 mergeable: true, 1443 contexts: []Context{ 1444 { 1445 Context: githubql.String("pj-a"), 1446 State: githubql.StatusStateSuccess, 1447 }, 1448 { 1449 Context: githubql.String("pj-b"), 1450 State: githubql.StatusStatePending, 1451 }, 1452 { 1453 Context: githubql.String("other-a"), 1454 State: githubql.StatusStateSuccess, 1455 }, 1456 }, 1457 }, 1458 }, 1459 expectedPRs: []int{2}, 1460 }, 1461 { 1462 name: "one mergeable PR failing PJ context (consider failing)", 1463 prs: []pr{ 1464 { 1465 number: 2, 1466 mergeable: true, 1467 contexts: []Context{ 1468 { 1469 Context: githubql.String("pj-a"), 1470 State: githubql.StatusStateSuccess, 1471 }, 1472 { 1473 Context: githubql.String("pj-b"), 1474 State: githubql.StatusStateFailure, 1475 }, 1476 { 1477 Context: githubql.String("other-a"), 1478 State: githubql.StatusStateSuccess, 1479 }, 1480 }, 1481 }, 1482 }, 1483 expectedPRs: []int{}, 1484 }, 1485 { 1486 name: "one mergeable PR missing PJ context (consider failing)", 1487 prs: []pr{ 1488 { 1489 number: 2, 1490 mergeable: true, 1491 contexts: []Context{ 1492 { 1493 Context: githubql.String("pj-b"), 1494 State: githubql.StatusStateSuccess, 1495 }, 1496 { 1497 Context: githubql.String("other-a"), 1498 State: githubql.StatusStateSuccess, 1499 }, 1500 }, 1501 }, 1502 }, 1503 expectedPRs: []int{}, 1504 }, 1505 { 1506 name: "one mergeable PR failing unknown context (consider in pool)", 1507 prs: []pr{ 1508 { 1509 number: 2, 1510 mergeable: true, 1511 contexts: []Context{ 1512 { 1513 Context: githubql.String("pj-a"), 1514 State: githubql.StatusStateSuccess, 1515 }, 1516 { 1517 Context: githubql.String("pj-b"), 1518 State: githubql.StatusStateSuccess, 1519 }, 1520 { 1521 Context: githubql.String("other-a"), 1522 State: githubql.StatusStateSuccess, 1523 }, 1524 { 1525 Context: githubql.String("unknown"), 1526 State: githubql.StatusStateFailure, 1527 }, 1528 }, 1529 }, 1530 }, 1531 expectedPRs: []int{2}, 1532 }, 1533 { 1534 name: "one PR failing non-PJ required context; one PR successful (should not prune pool)", 1535 prs: []pr{ 1536 { 1537 number: 1, 1538 mergeable: true, 1539 contexts: []Context{ 1540 { 1541 Context: githubql.String("pj-a"), 1542 State: githubql.StatusStateSuccess, 1543 }, 1544 { 1545 Context: githubql.String("pj-b"), 1546 State: githubql.StatusStateSuccess, 1547 }, 1548 { 1549 Context: githubql.String("other-a"), 1550 State: githubql.StatusStateFailure, 1551 }, 1552 }, 1553 }, 1554 { 1555 number: 2, 1556 mergeable: true, 1557 contexts: []Context{ 1558 { 1559 Context: githubql.String("pj-a"), 1560 State: githubql.StatusStateSuccess, 1561 }, 1562 { 1563 Context: githubql.String("pj-b"), 1564 State: githubql.StatusStateSuccess, 1565 }, 1566 { 1567 Context: githubql.String("other-a"), 1568 State: githubql.StatusStateSuccess, 1569 }, 1570 { 1571 Context: githubql.String("unknown"), 1572 State: githubql.StatusStateSuccess, 1573 }, 1574 }, 1575 }, 1576 }, 1577 expectedPRs: []int{2}, 1578 }, 1579 { 1580 name: "two successful PRs", 1581 prs: []pr{ 1582 { 1583 number: 1, 1584 mergeable: true, 1585 contexts: []Context{ 1586 { 1587 Context: githubql.String("pj-a"), 1588 State: githubql.StatusStateSuccess, 1589 }, 1590 { 1591 Context: githubql.String("pj-b"), 1592 State: githubql.StatusStateSuccess, 1593 }, 1594 { 1595 Context: githubql.String("other-a"), 1596 State: githubql.StatusStateSuccess, 1597 }, 1598 }, 1599 }, 1600 { 1601 number: 2, 1602 mergeable: true, 1603 contexts: []Context{ 1604 { 1605 Context: githubql.String("pj-a"), 1606 State: githubql.StatusStateSuccess, 1607 }, 1608 { 1609 Context: githubql.String("pj-b"), 1610 State: githubql.StatusStateSuccess, 1611 }, 1612 { 1613 Context: githubql.String("other-a"), 1614 State: githubql.StatusStateSuccess, 1615 }, 1616 }, 1617 }, 1618 }, 1619 expectedPRs: []int{1, 2}, 1620 }, 1621 } 1622 for _, tc := range tcs { 1623 t.Run(tc.name, func(t *testing.T) { 1624 sp := &subpool{ 1625 org: "org", 1626 repo: "repo", 1627 branch: "branch", 1628 presubmitContexts: presubmits, 1629 cc: cc, 1630 log: logrus.WithFields(logrus.Fields{"org": "org", "repo": "repo", "branch": "branch"}), 1631 } 1632 for _, pull := range tc.prs { 1633 pr := PullRequest{ 1634 Number: githubql.Int(pull.number), 1635 } 1636 pr.Commits.Nodes = []struct{ Commit Commit }{ 1637 { 1638 Commit{ 1639 Status: struct{ Contexts []Context }{ 1640 Contexts: pull.contexts, 1641 }, 1642 }, 1643 }, 1644 } 1645 if !pull.mergeable { 1646 pr.Mergeable = githubql.MergeableStateConflicting 1647 } 1648 sp.prs = append(sp.prs, pr) 1649 } 1650 1651 filtered := filterSubpool(nil, sp) 1652 if len(tc.expectedPRs) == 0 { 1653 if filtered != nil { 1654 t.Fatalf("Expected subpool to be pruned, but got: %v", filtered) 1655 } 1656 return 1657 } 1658 if filtered == nil { 1659 t.Fatalf("Expected subpool to have %d prs, but it was pruned.", len(tc.expectedPRs)) 1660 } 1661 if got := prNumbers(filtered.prs); !reflect.DeepEqual(got, tc.expectedPRs) { 1662 t.Errorf("Expected filtered pool to have PRs %v, but got %v.", tc.expectedPRs, got) 1663 } 1664 }) 1665 } 1666 } 1667 1668 func TestIsPassing(t *testing.T) { 1669 yes := true 1670 no := false 1671 headSHA := "head" 1672 success := string(githubql.StatusStateSuccess) 1673 failure := string(githubql.StatusStateFailure) 1674 testCases := []struct { 1675 name string 1676 passing bool 1677 config config.TideContextPolicy 1678 combinedContexts map[string]string 1679 }{ 1680 { 1681 name: "empty policy - success (trust combined status)", 1682 passing: true, 1683 combinedContexts: map[string]string{"c1": success, "c2": success, statusContext: failure}, 1684 }, 1685 { 1686 name: "empty policy - failure because of failed context c4 (trust combined status)", 1687 passing: false, 1688 combinedContexts: map[string]string{"c1": success, "c2": success, "c3": failure, statusContext: failure}, 1689 }, 1690 { 1691 name: "passing (trust combined status)", 1692 passing: true, 1693 config: config.TideContextPolicy{ 1694 RequiredContexts: []string{"c1", "c2", "c3"}, 1695 SkipUnknownContexts: &no, 1696 }, 1697 combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, statusContext: failure}, 1698 }, 1699 { 1700 name: "failing because of missing required check c3", 1701 passing: false, 1702 config: config.TideContextPolicy{ 1703 RequiredContexts: []string{"c1", "c2", "c3"}, 1704 }, 1705 combinedContexts: map[string]string{"c1": success, "c2": success, statusContext: failure}, 1706 }, 1707 { 1708 name: "failing because of failed context c2", 1709 passing: false, 1710 combinedContexts: map[string]string{"c1": success, "c2": failure}, 1711 config: config.TideContextPolicy{ 1712 RequiredContexts: []string{"c1", "c2", "c3"}, 1713 OptionalContexts: []string{"c4"}, 1714 }, 1715 }, 1716 { 1717 name: "passing because of failed context c4 is optional", 1718 passing: true, 1719 1720 combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure}, 1721 config: config.TideContextPolicy{ 1722 RequiredContexts: []string{"c1", "c2", "c3"}, 1723 OptionalContexts: []string{"c4"}, 1724 }, 1725 }, 1726 { 1727 name: "skipping unknown contexts - failing because of missing required context c3", 1728 passing: false, 1729 config: config.TideContextPolicy{ 1730 RequiredContexts: []string{"c1", "c2", "c3"}, 1731 SkipUnknownContexts: &yes, 1732 }, 1733 combinedContexts: map[string]string{"c1": success, "c2": success, statusContext: failure}, 1734 }, 1735 { 1736 name: "skipping unknown contexts - failing because c2 is failing", 1737 passing: false, 1738 combinedContexts: map[string]string{"c1": success, "c2": failure}, 1739 config: config.TideContextPolicy{ 1740 RequiredContexts: []string{"c1", "c2"}, 1741 OptionalContexts: []string{"c4"}, 1742 SkipUnknownContexts: &yes, 1743 }, 1744 }, 1745 { 1746 name: "skipping unknown contexts - passing because c4 is optional", 1747 passing: true, 1748 combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure}, 1749 config: config.TideContextPolicy{ 1750 RequiredContexts: []string{"c1", "c3"}, 1751 OptionalContexts: []string{"c4"}, 1752 SkipUnknownContexts: &yes, 1753 }, 1754 }, 1755 { 1756 name: "skipping unknown contexts - passing because c4 is optional and c5 is unknown", 1757 passing: true, 1758 1759 combinedContexts: map[string]string{"c1": success, "c2": success, "c3": success, "c4": failure, "c5": failure}, 1760 config: config.TideContextPolicy{ 1761 RequiredContexts: []string{"c1", "c3"}, 1762 OptionalContexts: []string{"c4"}, 1763 SkipUnknownContexts: &yes, 1764 }, 1765 }, 1766 } 1767 1768 for _, tc := range testCases { 1769 ghc := &fgc{ 1770 combinedStatus: tc.combinedContexts, 1771 expectedSHA: headSHA} 1772 log := logrus.WithField("component", "tide") 1773 _, err := log.String() 1774 if err != nil { 1775 t.Errorf("Failed to get log output before testing: %v", err) 1776 t.FailNow() 1777 } 1778 pr := PullRequest{HeadRefOID: githubql.String(headSHA)} 1779 passing := isPassingTests(log, ghc, pr, &tc.config) 1780 if passing != tc.passing { 1781 t.Errorf("%s: Expected %t got %t", tc.name, tc.passing, passing) 1782 } 1783 } 1784 } 1785 1786 func TestPresubmitsByPull(t *testing.T) { 1787 samplePR := PullRequest{ 1788 Number: githubql.Int(100), 1789 HeadRefOID: githubql.String("sha"), 1790 } 1791 testcases := []struct { 1792 name string 1793 1794 initialChangeCache map[changeCacheKey][]string 1795 presubmits []config.Presubmit 1796 1797 expectedPresubmits map[int]sets.String 1798 expectedChangeCache map[changeCacheKey][]string 1799 }{ 1800 { 1801 name: "no matching presubmits", 1802 presubmits: []config.Presubmit{ 1803 { 1804 Context: "always", 1805 RegexpChangeMatcher: config.RegexpChangeMatcher{ 1806 RunIfChanged: "foo", 1807 }, 1808 }, 1809 { 1810 Context: "never", 1811 }, 1812 }, 1813 expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"CHANGED"}}, 1814 expectedPresubmits: map[int]sets.String{}, 1815 }, 1816 { 1817 name: "no presubmits", 1818 presubmits: []config.Presubmit{}, 1819 expectedPresubmits: map[int]sets.String{}, 1820 }, 1821 { 1822 name: "no matching presubmits (check cache eviction)", 1823 presubmits: []config.Presubmit{ 1824 { 1825 Context: "never", 1826 }, 1827 }, 1828 initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 1829 expectedPresubmits: map[int]sets.String{}, 1830 }, 1831 { 1832 name: "no matching presubmits (check cache retention)", 1833 presubmits: []config.Presubmit{ 1834 { 1835 Context: "always", 1836 RegexpChangeMatcher: config.RegexpChangeMatcher{ 1837 RunIfChanged: "foo", 1838 }, 1839 }, 1840 { 1841 Context: "never", 1842 }, 1843 }, 1844 initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 1845 expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 1846 expectedPresubmits: map[int]sets.String{}, 1847 }, 1848 { 1849 name: "always_run", 1850 presubmits: []config.Presubmit{ 1851 { 1852 Context: "always", 1853 AlwaysRun: true, 1854 }, 1855 { 1856 Context: "never", 1857 }, 1858 }, 1859 expectedPresubmits: map[int]sets.String{100: sets.NewString("always")}, 1860 }, 1861 { 1862 name: "runs against branch", 1863 presubmits: []config.Presubmit{ 1864 { 1865 Context: "presubmit", 1866 AlwaysRun: true, 1867 Brancher: config.Brancher{ 1868 Branches: []string{"master", "dev"}, 1869 }, 1870 }, 1871 { 1872 Context: "never", 1873 }, 1874 }, 1875 expectedPresubmits: map[int]sets.String{100: sets.NewString("presubmit")}, 1876 }, 1877 { 1878 name: "doesn't run against branch", 1879 presubmits: []config.Presubmit{ 1880 { 1881 Context: "presubmit", 1882 AlwaysRun: true, 1883 Brancher: config.Brancher{ 1884 Branches: []string{"release", "dev"}, 1885 }, 1886 }, 1887 { 1888 Context: "always", 1889 AlwaysRun: true, 1890 }, 1891 { 1892 Context: "never", 1893 }, 1894 }, 1895 expectedPresubmits: map[int]sets.String{100: sets.NewString("always")}, 1896 }, 1897 { 1898 name: "run_if_changed (uncached)", 1899 presubmits: []config.Presubmit{ 1900 { 1901 Context: "presubmit", 1902 RegexpChangeMatcher: config.RegexpChangeMatcher{ 1903 RunIfChanged: "^CHANGE.$", 1904 }, 1905 }, 1906 { 1907 Context: "always", 1908 AlwaysRun: true, 1909 }, 1910 { 1911 Context: "never", 1912 }, 1913 }, 1914 expectedPresubmits: map[int]sets.String{100: sets.NewString("presubmit", "always")}, 1915 expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"CHANGED"}}, 1916 }, 1917 { 1918 name: "run_if_changed (cached)", 1919 presubmits: []config.Presubmit{ 1920 { 1921 Context: "presubmit", 1922 RegexpChangeMatcher: config.RegexpChangeMatcher{ 1923 RunIfChanged: "^FIL.$", 1924 }, 1925 }, 1926 { 1927 Context: "always", 1928 AlwaysRun: true, 1929 }, 1930 { 1931 Context: "never", 1932 }, 1933 }, 1934 initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 1935 expectedPresubmits: map[int]sets.String{100: sets.NewString("presubmit", "always")}, 1936 expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 1937 }, 1938 { 1939 name: "run_if_changed (cached) (skippable)", 1940 presubmits: []config.Presubmit{ 1941 { 1942 Context: "presubmit", 1943 RegexpChangeMatcher: config.RegexpChangeMatcher{ 1944 RunIfChanged: "^CHANGE.$", 1945 }, 1946 }, 1947 { 1948 Context: "always", 1949 AlwaysRun: true, 1950 }, 1951 { 1952 Context: "never", 1953 }, 1954 }, 1955 initialChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 1956 expectedPresubmits: map[int]sets.String{100: sets.NewString("always")}, 1957 expectedChangeCache: map[changeCacheKey][]string{{number: 100, sha: "sha"}: {"FILE"}}, 1958 }, 1959 } 1960 1961 for _, tc := range testcases { 1962 t.Logf("Starting test case: %q", tc.name) 1963 1964 if tc.initialChangeCache == nil { 1965 tc.initialChangeCache = map[changeCacheKey][]string{} 1966 } 1967 if tc.expectedChangeCache == nil { 1968 tc.expectedChangeCache = map[changeCacheKey][]string{} 1969 } 1970 1971 cfg := &config.Config{} 1972 cfg.SetPresubmits(map[string][]config.Presubmit{ 1973 "/": tc.presubmits, 1974 "foo/bar": {{Context: "wrong-repo", AlwaysRun: true}}, 1975 }) 1976 cfgAgent := &config.Agent{} 1977 cfgAgent.Set(cfg) 1978 sp := &subpool{ 1979 branch: "master", 1980 prs: []PullRequest{samplePR}, 1981 } 1982 c := &Controller{ 1983 ca: cfgAgent, 1984 ghc: &fgc{}, 1985 changedFiles: &changedFilesAgent{ 1986 ghc: &fgc{}, 1987 changeCache: tc.initialChangeCache, 1988 nextChangeCache: make(map[changeCacheKey][]string), 1989 }, 1990 } 1991 presubmits, err := c.presubmitsByPull(sp) 1992 if err != nil { 1993 t.Fatalf("unexpected error from presubmitsByPull: %v", err) 1994 } 1995 c.changedFiles.prune() 1996 if !reflect.DeepEqual(presubmits, tc.expectedPresubmits) { 1997 t.Errorf("expected presubmit mapping: %v,\nbut got %v\n", tc.expectedPresubmits, presubmits) 1998 } 1999 if got := c.changedFiles.changeCache; !reflect.DeepEqual(got, tc.expectedChangeCache) { 2000 t.Errorf("expected file change cache: %v,\nbut got %v\n", tc.expectedChangeCache, got) 2001 } 2002 } 2003 }