github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/github/report/report_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 report 18 19 import ( 20 "context" 21 "fmt" 22 "strconv" 23 "strings" 24 "testing" 25 "text/template" 26 27 "github.com/google/go-cmp/cmp" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 30 "sigs.k8s.io/prow/pkg/config" 31 "sigs.k8s.io/prow/pkg/github" 32 "sigs.k8s.io/prow/pkg/kube" 33 ) 34 35 func TestParseIssueComment(t *testing.T) { 36 var testcases = []struct { 37 name string 38 context string 39 state string 40 ics []github.IssueComment 41 expectedDeletes []int 42 expectedEntries []string 43 expectedUpdate int 44 isOptional bool 45 }{ 46 { 47 name: "should create a new comment", 48 context: "bla test", 49 state: github.StatusFailure, 50 expectedEntries: []string{createReportEntry("bla test", true)}, 51 }, 52 { 53 name: "should create a new optional comment", 54 context: "bla test", 55 state: github.StatusFailure, 56 isOptional: true, 57 expectedEntries: []string{createReportEntry("bla test", false)}, 58 }, 59 { 60 name: "should not delete an up-to-date comment", 61 context: "bla test", 62 state: github.StatusSuccess, 63 ics: []github.IssueComment{ 64 { 65 User: github.User{Login: "k8s-ci-robot"}, 66 Body: "--- | --- | ---\nfoo test | something | or other\n\n", 67 }, 68 }, 69 }, 70 { 71 name: "should delete when all tests pass", 72 context: "bla test", 73 state: github.StatusSuccess, 74 ics: []github.IssueComment{ 75 { 76 User: github.User{Login: "k8s-ci-robot"}, 77 Body: "--- | --- | ---\nbla test | something | or other\n\n" + commentTag, 78 ID: 123, 79 }, 80 }, 81 expectedDeletes: []int{123}, 82 expectedEntries: []string{}, 83 }, 84 { 85 name: "should delete a passing test with \\r", 86 context: "bla test", 87 state: github.StatusSuccess, 88 ics: []github.IssueComment{ 89 { 90 User: github.User{Login: "k8s-ci-robot"}, 91 Body: "--- | --- | ---\r\nbla test | something | or other\r\n\r\n" + commentTag, 92 ID: 123, 93 }, 94 }, 95 expectedDeletes: []int{123}, 96 expectedEntries: []string{}, 97 }, 98 99 { 100 name: "should update a failed test", 101 context: "bla test", 102 state: github.StatusFailure, 103 ics: []github.IssueComment{ 104 { 105 User: github.User{Login: "k8s-ci-robot"}, 106 Body: "--- | --- | ---\nbla test | something | or other\n\n" + commentTag, 107 ID: 123, 108 }, 109 }, 110 expectedDeletes: []int{123}, 111 expectedEntries: []string{"bla test"}, 112 }, 113 { 114 name: "should preserve old results when updating", 115 context: "bla test", 116 state: github.StatusFailure, 117 ics: []github.IssueComment{ 118 { 119 User: github.User{Login: "k8s-ci-robot"}, 120 Body: "--- | --- | ---\nbla test | something | or other\nfoo test | wow | aye\n\n" + commentTag, 121 ID: 123, 122 }, 123 }, 124 expectedDeletes: []int{123}, 125 expectedEntries: []string{"bla test", "foo test"}, 126 }, 127 { 128 name: "should merge duplicates", 129 context: "bla test", 130 state: github.StatusFailure, 131 ics: []github.IssueComment{ 132 { 133 User: github.User{Login: "k8s-ci-robot"}, 134 Body: "--- | --- | ---\nbla test | something | or other\nfoo test | wow such\n\n" + commentTag, 135 ID: 123, 136 }, 137 { 138 User: github.User{Login: "k8s-ci-robot"}, 139 Body: "--- | --- | ---\nfoo test | beep | boop\n\n" + commentTag, 140 ID: 124, 141 }, 142 }, 143 expectedDeletes: []int{123, 124}, 144 expectedEntries: []string{"bla test", "foo test"}, 145 }, 146 { 147 name: "should update an old comment when a test passes", 148 context: "bla test", 149 state: github.StatusSuccess, 150 ics: []github.IssueComment{ 151 { 152 User: github.User{Login: "k8s-ci-robot"}, 153 Body: "--- | --- | ---\nbla test | something | or other\nfoo test | wow | aye\n\n" + commentTag, 154 ID: 123, 155 }, 156 }, 157 expectedDeletes: []int{}, 158 expectedEntries: []string{"foo test"}, 159 expectedUpdate: 123, 160 }, 161 } 162 for _, tc := range testcases { 163 t.Run(tc.name, func(t *testing.T) { 164 pj := prowapi.ProwJob{ 165 ObjectMeta: metav1.ObjectMeta{ 166 Labels: map[string]string{ 167 kube.IsOptionalLabel: strconv.FormatBool(tc.isOptional), 168 }, 169 }, 170 Spec: prowapi.ProwJobSpec{ 171 Type: prowapi.PresubmitJob, 172 Context: tc.context, 173 Refs: &prowapi.Refs{Pulls: []prowapi.Pull{{}}}, 174 }, 175 Status: prowapi.ProwJobStatus{ 176 State: prowapi.ProwJobState(tc.state), 177 }, 178 } 179 isBot := func(candidate string) bool { 180 return candidate == "k8s-ci-robot" 181 } 182 deletes, entries, update := parseIssueComments([]prowapi.ProwJob{pj}, isBot, tc.ics) 183 if len(deletes) != len(tc.expectedDeletes) { 184 t.Errorf("It %q: wrong number of deletes. Got %v, expected %v", tc.name, deletes, tc.expectedDeletes) 185 } else { 186 for _, edel := range tc.expectedDeletes { 187 found := false 188 for _, del := range deletes { 189 if del == edel { 190 found = true 191 break 192 } 193 } 194 if !found { 195 t.Errorf("It %q: expected to find %d in %v", tc.name, edel, deletes) 196 } 197 } 198 } 199 if len(entries) != len(tc.expectedEntries) { 200 t.Errorf("It %q: wrong number of entries. Got %v, expected %v", tc.name, entries, tc.expectedEntries) 201 } 202 if tc.expectedUpdate != update { 203 t.Errorf("It %q: expected update %d, got %d", tc.name, tc.expectedUpdate, update) 204 } 205 206 for _, expectedEntry := range tc.expectedEntries { 207 found := false 208 for _, ent := range entries { 209 if strings.Contains(ent, expectedEntry) { 210 found = true 211 break 212 } 213 } 214 if !found { 215 t.Errorf("It %q: expected to find %q in %v", tc.name, expectedEntry, entries) 216 } 217 } 218 }) 219 } 220 } 221 222 func createReportEntry(context string, isRequired bool) string { 223 return fmt.Sprintf("%s | | [link]() | %s | ", context, strconv.FormatBool(isRequired)) 224 } 225 226 type fakeGhClient struct { 227 status []github.Status 228 comments []string 229 } 230 231 func (gh fakeGhClient) BotUserCheckerWithContext(_ context.Context) (func(string) bool, error) { 232 return func(candidate string) bool { 233 return candidate == "BotName" 234 }, nil 235 } 236 237 const maxLen = 140 238 239 func (gh *fakeGhClient) CreateStatusWithContext(_ context.Context, org, repo, ref string, s github.Status) error { 240 if d := s.Description; len(d) > maxLen { 241 return fmt.Errorf("%s is len %d, more than max of %d chars", d, len(d), maxLen) 242 } 243 gh.status = append(gh.status, s) 244 return nil 245 246 } 247 func (gh fakeGhClient) ListIssueCommentsWithContext(_ context.Context, org, repo string, number int) ([]github.IssueComment, error) { 248 return nil, nil 249 } 250 func (gh *fakeGhClient) CreateCommentWithContext(_ context.Context, org, repo string, number int, comment string) error { 251 gh.comments = append(gh.comments, comment) 252 return nil 253 } 254 func (gh fakeGhClient) DeleteCommentWithContext(_ context.Context, org, repo string, ID int) error { 255 return nil 256 } 257 func (gh fakeGhClient) EditCommentWithContext(_ context.Context, org, repo string, ID int, comment string) error { 258 return nil 259 } 260 261 func shout(i int) string { 262 if i == 0 { 263 return "start" 264 } 265 return fmt.Sprintf("%s part%d", shout(i-1), i) 266 } 267 268 func TestReportStatus(t *testing.T) { 269 const ( 270 defMsg = "default-message" 271 ) 272 tests := []struct { 273 name string 274 275 state prowapi.ProwJobState 276 report bool 277 desc string // override default msg 278 pjType prowapi.ProwJobType 279 expectedStatuses []string 280 expectedDesc string 281 }{ 282 { 283 name: "Successful prowjob with report true should set status", 284 285 state: prowapi.SuccessState, 286 pjType: prowapi.PresubmitJob, 287 report: true, 288 expectedStatuses: []string{"success"}, 289 }, 290 { 291 name: "Successful prowjob with report false should not set status", 292 293 state: prowapi.SuccessState, 294 pjType: prowapi.PresubmitJob, 295 report: false, 296 expectedStatuses: []string{}, 297 }, 298 { 299 name: "Pending prowjob with report true should set status", 300 301 state: prowapi.PendingState, 302 report: true, 303 pjType: prowapi.PresubmitJob, 304 expectedStatuses: []string{"pending"}, 305 }, 306 { 307 name: "Aborted presubmit job with report true should set failure status", 308 309 state: prowapi.AbortedState, 310 report: true, 311 pjType: prowapi.PresubmitJob, 312 expectedStatuses: []string{"failure"}, 313 }, 314 { 315 name: "Triggered presubmit job with report true should set pending status", 316 317 state: prowapi.TriggeredState, 318 report: true, 319 pjType: prowapi.PresubmitJob, 320 expectedStatuses: []string{"pending"}, 321 }, 322 { 323 name: "really long description is truncated", 324 325 state: prowapi.TriggeredState, 326 report: true, 327 expectedStatuses: []string{"pending"}, 328 desc: shout(maxLen), // resulting string will exceed maxLen 329 expectedDesc: config.ContextDescriptionWithBaseSha(shout(maxLen), ""), 330 }, 331 { 332 name: "Successful postsubmit job with report true should set success status", 333 334 state: prowapi.SuccessState, 335 report: true, 336 pjType: prowapi.PostsubmitJob, 337 338 expectedStatuses: []string{"success"}, 339 }, 340 } 341 342 for _, tc := range tests { 343 t.Run(tc.name, func(t *testing.T) { 344 // Setup 345 ghc := &fakeGhClient{} 346 347 if tc.desc == "" { 348 tc.desc = defMsg 349 } 350 if tc.expectedDesc == "" { 351 tc.expectedDesc = defMsg 352 } 353 pj := prowapi.ProwJob{ 354 Status: prowapi.ProwJobStatus{ 355 State: tc.state, 356 Description: tc.desc, 357 URL: "http://mytest.com", 358 }, 359 Spec: prowapi.ProwJobSpec{ 360 Job: "job-name", 361 Type: tc.pjType, 362 Context: "parent", 363 Report: tc.report, 364 Refs: &prowapi.Refs{ 365 Org: "k8s", 366 Repo: "test-infra", 367 Pulls: []prowapi.Pull{{ 368 Author: "me", 369 Number: 1, 370 SHA: "abcdef", 371 HeadRef: "fixes-123", 372 }}, 373 }, 374 }, 375 } 376 // Run 377 if err := reportStatus(context.Background(), ghc, pj); err != nil { 378 t.Error(err) 379 } 380 // Check 381 if len(ghc.status) != len(tc.expectedStatuses) { 382 t.Errorf("expected %d status(es), found %d", len(tc.expectedStatuses), len(ghc.status)) 383 return 384 } 385 for i, status := range ghc.status { 386 if status.State != tc.expectedStatuses[i] { 387 t.Errorf("unexpected status: %s, expected: %s", status.State, tc.expectedStatuses[i]) 388 } 389 if i == 0 && status.Description != tc.expectedDesc { 390 t.Errorf("description %d %s != expected %s", i, status.Description, tc.expectedDesc) 391 } 392 } 393 }) 394 } 395 } 396 397 func TestShouldReport(t *testing.T) { 398 var testcases = []struct { 399 name string 400 pj prowapi.ProwJob 401 validTypes []prowapi.ProwJobType 402 report bool 403 }{ 404 { 405 name: "should not report skip report job", 406 pj: prowapi.ProwJob{ 407 Spec: prowapi.ProwJobSpec{ 408 Type: prowapi.PresubmitJob, 409 Report: false, 410 }, 411 }, 412 validTypes: []prowapi.ProwJobType{prowapi.PresubmitJob}, 413 }, 414 { 415 name: "should report presubmit job", 416 pj: prowapi.ProwJob{ 417 Spec: prowapi.ProwJobSpec{ 418 Type: prowapi.PresubmitJob, 419 Report: true, 420 }, 421 }, 422 validTypes: []prowapi.ProwJobType{prowapi.PresubmitJob}, 423 report: true, 424 }, 425 { 426 name: "should not report postsubmit job", 427 pj: prowapi.ProwJob{ 428 Spec: prowapi.ProwJobSpec{ 429 Type: prowapi.PostsubmitJob, 430 Report: true, 431 }, 432 }, 433 validTypes: []prowapi.ProwJobType{prowapi.PresubmitJob}, 434 }, 435 { 436 name: "should report postsubmit job if told to", 437 pj: prowapi.ProwJob{ 438 Spec: prowapi.ProwJobSpec{ 439 Type: prowapi.PostsubmitJob, 440 Report: true, 441 }, 442 }, 443 validTypes: []prowapi.ProwJobType{prowapi.PresubmitJob, prowapi.PostsubmitJob}, 444 report: true, 445 }, 446 } 447 448 for _, tc := range testcases { 449 r := ShouldReport(tc.pj, tc.validTypes) 450 451 if r != tc.report { 452 t.Errorf("Unexpected result from test: %s.\nExpected: %v\nGot: %v", 453 tc.name, tc.report, r) 454 } 455 } 456 } 457 458 func TestCreateComment(t *testing.T) { 459 tests := []struct { 460 name string 461 template *template.Template 462 pjs []prowapi.ProwJob 463 entries []string 464 want string 465 wantErr bool 466 }{ 467 { 468 name: "single-job-single-failure", 469 template: mustParseTemplate(t, ""), 470 pjs: []prowapi.ProwJob{ 471 { 472 Spec: prowapi.ProwJobSpec{ 473 Refs: &prowapi.Refs{ 474 Pulls: []prowapi.Pull{ 475 { 476 Author: "chaodaig", 477 }, 478 }, 479 }, 480 }, 481 }, 482 }, 483 entries: []string{ 484 "aaa | bbb | ccc | ddd | eee", 485 }, 486 want: `@chaodaig: The following test **failed**, say ` + "`/retest`" + ` to rerun all failed tests or ` + "`/retest-required`" + ` to rerun all mandatory failed tests: 487 488 Test name | Commit | Details | Required | Rerun command 489 --- | --- | --- | --- | --- 490 aaa | bbb | ccc | ddd | eee 491 492 493 494 <details> 495 496 Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md). If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands). 497 </details> 498 <!-- test report -->`, 499 }, 500 { 501 name: "single-job-multi;e-failure", 502 template: mustParseTemplate(t, ""), 503 pjs: []prowapi.ProwJob{ 504 { 505 Spec: prowapi.ProwJobSpec{ 506 Refs: &prowapi.Refs{ 507 Pulls: []prowapi.Pull{ 508 { 509 Author: "chaodaig", 510 }, 511 }, 512 }, 513 }, 514 }, 515 }, 516 entries: []string{ 517 "aaa | bbb | ccc | ddd | eee", 518 "fff | ggg | hhh | iii | jjj", 519 }, 520 want: `@chaodaig: The following tests **failed**, say ` + "`/retest`" + ` to rerun all failed tests or ` + "`/retest-required`" + ` to rerun all mandatory failed tests: 521 522 Test name | Commit | Details | Required | Rerun command 523 --- | --- | --- | --- | --- 524 aaa | bbb | ccc | ddd | eee 525 fff | ggg | hhh | iii | jjj 526 527 528 529 <details> 530 531 Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md). If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands). 532 </details> 533 <!-- test report -->`, 534 }, 535 { 536 name: "multiple-job-only-use-first-one", 537 template: mustParseTemplate(t, "{{.Spec.Job}}"), 538 pjs: []prowapi.ProwJob{ 539 { 540 Spec: prowapi.ProwJobSpec{ 541 Job: "job-a", 542 Refs: &prowapi.Refs{ 543 Pulls: []prowapi.Pull{ 544 { 545 Author: "chaodaig", 546 }, 547 }, 548 }, 549 }, 550 }, 551 { 552 Spec: prowapi.ProwJobSpec{ 553 Job: "job-b", 554 Refs: &prowapi.Refs{ 555 Pulls: []prowapi.Pull{ 556 { 557 Author: "chaodaig", 558 }, 559 }, 560 }, 561 }, 562 }, 563 }, 564 entries: []string{ 565 "aaa | bbb | ccc | ddd | eee", 566 }, 567 want: `@chaodaig: The following test **failed**, say ` + "`/retest`" + ` to rerun all failed tests or ` + "`/retest-required`" + ` to rerun all mandatory failed tests: 568 569 Test name | Commit | Details | Required | Rerun command 570 --- | --- | --- | --- | --- 571 aaa | bbb | ccc | ddd | eee 572 573 job-a 574 575 <details> 576 577 Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md). If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands). 578 </details> 579 <!-- test report -->`, 580 }, 581 { 582 name: "multiple-job-all-passed", 583 pjs: []prowapi.ProwJob{ 584 { 585 Spec: prowapi.ProwJobSpec{ 586 Job: "job-a", 587 Refs: &prowapi.Refs{ 588 Pulls: []prowapi.Pull{ 589 { 590 Author: "chaodaig", 591 }, 592 }, 593 }, 594 }, 595 }, 596 { 597 Spec: prowapi.ProwJobSpec{ 598 Job: "job-b", 599 Refs: &prowapi.Refs{ 600 Pulls: []prowapi.Pull{ 601 { 602 Author: "chaodaig", 603 }, 604 }, 605 }, 606 }, 607 }, 608 }, 609 entries: []string{}, 610 want: `@chaodaig: all tests **passed!** 611 612 613 <details> 614 615 Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md). If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands). 616 </details> 617 <!-- test report -->`, 618 }, 619 } 620 621 for _, tc := range tests { 622 t.Run(tc.name, func(t *testing.T) { 623 gotComment, gotErr := createComment(tc.template, tc.pjs, tc.entries) 624 if diff := cmp.Diff(gotComment, tc.want); diff != "" { 625 t.Fatalf("comment mismatch:\n%s", diff) 626 } 627 if (gotErr != nil && !tc.wantErr) || (gotErr == nil && tc.wantErr) { 628 t.Fatalf("error mismatch. got: %v, want: %v", gotErr, tc.wantErr) 629 } 630 }) 631 } 632 } 633 634 func mustParseTemplate(t *testing.T, s string) *template.Template { 635 tmpl, err := template.New("test").Parse(s) 636 if err != nil { 637 t.Fatal(err) 638 } 639 return tmpl 640 } 641 642 func TestReportComment(t *testing.T) { 643 t.Parallel() 644 testCases := []struct { 645 name string 646 pjs []prowapi.ProwJob 647 reporterConfig config.GitHubReporter 648 mustCreate bool 649 expectedComment bool 650 }{ 651 { 652 name: "failed pj", 653 pjs: []prowapi.ProwJob{{ 654 Spec: prowapi.ProwJobSpec{ 655 Type: prowapi.PresubmitJob, 656 Report: true, 657 Refs: &prowapi.Refs{ 658 Pulls: []prowapi.Pull{{}}, 659 }, 660 }, 661 Status: prowapi.ProwJobStatus{ 662 State: prowapi.FailureState, 663 CompletionTime: &metav1.Time{}, 664 }}, 665 }, 666 reporterConfig: config.GitHubReporter{ 667 JobTypesToReport: []prowapi.ProwJobType{prowapi.PresubmitJob}, 668 }, 669 expectedComment: true, 670 }, 671 { 672 name: "succeeded pj when mustCreate is true", 673 pjs: []prowapi.ProwJob{{ 674 Spec: prowapi.ProwJobSpec{ 675 Type: prowapi.PresubmitJob, 676 Report: true, 677 Refs: &prowapi.Refs{ 678 Pulls: []prowapi.Pull{{}}, 679 }, 680 }, 681 Status: prowapi.ProwJobStatus{ 682 State: prowapi.SuccessState, 683 CompletionTime: &metav1.Time{}, 684 }}, 685 }, 686 reporterConfig: config.GitHubReporter{ 687 JobTypesToReport: []prowapi.ProwJobType{prowapi.PresubmitJob}, 688 }, 689 mustCreate: true, 690 expectedComment: true, 691 }, 692 { 693 name: "aborted pj when mustCreate is true", 694 pjs: []prowapi.ProwJob{{ 695 Spec: prowapi.ProwJobSpec{ 696 Type: prowapi.PresubmitJob, 697 Report: true, 698 Refs: &prowapi.Refs{ 699 Pulls: []prowapi.Pull{{}}, 700 }, 701 }, 702 Status: prowapi.ProwJobStatus{ 703 State: prowapi.AbortedState, 704 CompletionTime: &metav1.Time{}, 705 }}, 706 }, 707 reporterConfig: config.GitHubReporter{ 708 JobTypesToReport: []prowapi.ProwJobType{prowapi.PresubmitJob}, 709 }, 710 mustCreate: true, 711 }, 712 } 713 for _, tc := range testCases { 714 t.Run(tc.name, func(t *testing.T) { 715 fghc := &fakeGhClient{} 716 err := ReportComment(context.Background(), fghc, nil, tc.pjs, tc.reporterConfig, tc.mustCreate) 717 if err != nil { 718 t.Fatalf("unexpected error: %v", err) 719 } 720 721 if diff := cmp.Diff(tc.expectedComment, len(fghc.comments) == 1); diff != "" { 722 t.Fatalf("expectedComment didn't match result, diff: %s", diff) 723 } 724 }) 725 } 726 }