sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/spyglass/lenses/buildlog/lens_test.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package buildlog 18 19 import ( 20 "encoding/json" 21 "fmt" 22 stdio "io" 23 "net/http" 24 "net/http/httptest" 25 "strings" 26 "testing" 27 28 "github.com/google/go-cmp/cmp" 29 prowconfig "sigs.k8s.io/prow/pkg/config" 30 pkgio "sigs.k8s.io/prow/pkg/io" 31 "sigs.k8s.io/prow/pkg/spyglass/api" 32 "sigs.k8s.io/prow/pkg/spyglass/lenses/fake" 33 ) 34 35 func TestGetConfig(t *testing.T) { 36 def := parsedConfig{ 37 showRawLog: true, 38 } 39 cases := []struct { 40 name string 41 raw string 42 want parsedConfig 43 }{ 44 { 45 name: "empty", 46 want: def, 47 }, 48 { 49 name: "require highlighter endpoint", 50 raw: `{"highlighter": {"pin": true}}`, 51 want: def, 52 }, 53 { 54 name: "configure highligher", 55 raw: `{"highlighter": {"endpoint": "service", "pin": true}}`, 56 want: func() parsedConfig { 57 d := def 58 d.highlighter = &highlightConfig{ 59 Endpoint: "service", 60 Pin: true, 61 } 62 return d 63 }(), 64 }, 65 } 66 67 for _, tc := range cases { 68 t.Run(tc.name, func(t *testing.T) { 69 got := getConfig(json.RawMessage(tc.raw)) 70 got.highlightRegex = nil 71 if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(parsedConfig{}, highlightConfig{})); diff != "" { 72 t.Errorf("getConfig(%q) got unexpected diff (-want +got):\n%s", tc.raw, diff) 73 } 74 }) 75 } 76 } 77 78 func TestExpand(t *testing.T) { 79 cases := []struct { 80 name string 81 g LineGroup 82 want bool 83 }{ 84 { 85 name: "basic", 86 }, 87 { 88 name: "not enough", 89 g: LineGroup{ 90 LogLines: make([]LogLine, moreLines-1), 91 }, 92 }, 93 { 94 name: "just enough", 95 g: LineGroup{ 96 LogLines: make([]LogLine, moreLines), 97 }, 98 want: true, 99 }, 100 { 101 name: "more than enough", 102 g: LineGroup{ 103 LogLines: make([]LogLine, moreLines+1), 104 }, 105 want: true, 106 }, 107 } 108 109 for _, tc := range cases { 110 t.Run(tc.name, func(t *testing.T) { 111 if got := tc.g.Expand(); got != tc.want { 112 t.Errorf("Expand() got %t, wanted %t", got, tc.want) 113 } 114 }) 115 } 116 } 117 118 func TestGroupLines(t *testing.T) { 119 lorem := []string{ 120 "Lorem ipsum dolor sit amet", 121 "consectetur adipiscing elit", 122 "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", 123 "Ut enim ad minim veniam", 124 "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat", 125 "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur", 126 "Excepteur sint occaecat cupidatat non proident", 127 "sunt in culpa qui officia deserunt mollit anim id est laborum", 128 } 129 tests := []struct { 130 name string 131 lines []string 132 start int 133 end int 134 groups []LineGroup 135 }{ 136 { 137 name: "Test empty log", 138 lines: []string{}, 139 groups: []LineGroup{}, 140 }, 141 { 142 name: "Test error highlighting", 143 lines: []string{"This is an ErRoR message"}, 144 groups: []LineGroup{ 145 { 146 Start: 0, 147 End: 1, 148 Skip: false, 149 ByteOffset: 0, 150 ByteLength: 24, 151 }, 152 }, 153 }, 154 { 155 name: "Test skip all", 156 lines: lorem, 157 groups: []LineGroup{ 158 { 159 Start: 0, 160 End: 8, 161 Skip: true, 162 ByteOffset: 0, 163 ByteLength: 437, 164 }, 165 }, 166 }, 167 { 168 name: "Test skip none", 169 lines: []string{ 170 "a", "b", "c", "d", "e", 171 "ERROR: Failed to immanentize the eschaton.", 172 "a", "b", "c", "d", "e", 173 }, 174 groups: []LineGroup{ 175 { 176 Start: 0, 177 End: 11, 178 Skip: false, 179 ByteOffset: 0, 180 ByteLength: 62, 181 }, 182 }, 183 }, 184 { 185 name: "Test skip threshold", 186 lines: []string{ 187 "a", "b", "c", "d", // skip threshold unmet 188 "a", "b", "c", "d", "e", "ERROR: Failed to immanentize the eschaton.", "a", "b", "c", "d", "e", 189 "a", "b", "c", "d", "e", // skip threshold met 190 }, 191 groups: []LineGroup{ 192 { 193 Start: 0, 194 End: 4, 195 Skip: false, 196 ByteOffset: 0, 197 ByteLength: 7, 198 }, 199 { 200 Start: 4, 201 End: 15, 202 Skip: false, 203 ByteOffset: 8, 204 ByteLength: 62, 205 }, 206 { 207 Start: 15, 208 End: 20, 209 Skip: true, 210 ByteOffset: 71, 211 ByteLength: 9, 212 }, 213 }, 214 }, 215 { 216 name: "Test nearby errors", 217 lines: []string{ 218 "a", "b", "c", 219 "don't panic", 220 "a", "b", "c", 221 "don't panic", 222 "a", "b", "c", 223 }, 224 groups: []LineGroup{ 225 { 226 Start: 0, 227 End: 11, 228 Skip: false, 229 ByteOffset: 0, 230 ByteLength: 41, 231 }, 232 }, 233 }, 234 { 235 name: "Test separated errors", 236 lines: []string{ 237 "a", "b", "c", 238 "don't panic", 239 "a", "b", "c", "d", "e", 240 "a", "b", "c", 241 "a", "b", "c", "d", "e", 242 "don't panic", 243 "a", "b", "c", 244 }, 245 groups: []LineGroup{ 246 { 247 Start: 0, 248 End: 9, 249 Skip: false, 250 ByteOffset: 0, 251 ByteLength: 27, 252 }, 253 { 254 Start: 9, 255 End: 12, 256 Skip: false, 257 ByteOffset: 28, 258 ByteLength: 5, 259 }, 260 { 261 Start: 12, 262 End: 21, 263 Skip: false, 264 ByteOffset: 34, 265 ByteLength: 27, 266 }, 267 }, 268 }, 269 } 270 art := "fake-artifact" 271 for _, test := range tests { 272 t.Run(test.name, func(t *testing.T) { 273 got := groupLines(&art, test.start, test.end, highlightLines(test.lines, 0, &art, defaultErrRE, defaultHighlightLineLengthMax)...) 274 if len(got) != len(test.groups) { 275 t.Fatalf("Expected %d groups, got %d", len(test.groups), len(got)) 276 } 277 for j, exp := range test.groups { 278 if got[j].Start != exp.Start || got[j].End != exp.End { 279 t.Fatalf("Group %d expected lines [%d, %d), got [%d, %d)", j, exp.Start, exp.End, got[j].Start, got[j].End) 280 } 281 if got[j].Skip != exp.Skip { 282 t.Errorf("Lines [%d, %d) expected Skip = %t", exp.Start, exp.End, exp.Skip) 283 } 284 if got[j].ByteOffset != exp.ByteOffset { 285 t.Errorf("Group %d expected ByteOffset %d, got %d.", j, exp.ByteOffset, got[j].ByteOffset) 286 } 287 if got[j].ByteLength != exp.ByteLength { 288 t.Errorf("Group %d expected ByteLength %d, got %d.", j, exp.ByteLength, got[j].ByteLength) 289 } 290 } 291 }) 292 } 293 } 294 295 func pstr(s string) *string { return &s } 296 297 func TestBody(t *testing.T) { 298 const ( 299 anonLink = "https://storage.googleapis.com/bucket/object/build-log.txt" 300 cookieLink = "https://storage.cloud.google.com/bucket/object/build-log.txt" 301 ) 302 render := func(views ...LogArtifactView) string { 303 return executeTemplate(".", "body", buildLogsView{LogViews: views}) 304 } 305 view := func(name, link string, groups []LineGroup) LogArtifactView { 306 return LogArtifactView{ 307 ArtifactName: name, 308 ArtifactLink: link, 309 ViewAll: true, 310 LineGroups: groups, 311 ShowRawLog: true, 312 } 313 } 314 315 var hf http.HandlerFunc 316 317 server := httptest.NewServer(&hf) 318 defer server.Close() 319 320 cases := []struct { 321 name string 322 artifact *fake.Artifact 323 rawConfig json.RawMessage 324 highlighter func() (highlightRequest, int, string) 325 want string 326 }{ 327 { 328 name: "empty", 329 artifact: &fake.Artifact{ 330 Path: "foo", 331 Content: []byte(""), 332 }, 333 want: render(view("foo", fake.NotFound, []LineGroup{ 334 { 335 Start: 1, 336 End: 1, 337 ArtifactName: pstr("foo"), 338 LogLines: []LogLine{ 339 { 340 ArtifactName: pstr("foo"), 341 Number: 1, 342 SubLines: []SubLine{ 343 {}, 344 }, 345 }, 346 }, 347 }, 348 }, 349 )), 350 }, 351 { 352 name: "single", 353 artifact: &fake.Artifact{ 354 Path: "foo", 355 Content: []byte("hello"), 356 }, 357 want: render(view("foo", fake.NotFound, []LineGroup{ 358 { 359 Start: 1, 360 End: 1, 361 ArtifactName: pstr("foo"), 362 LogLines: []LogLine{ 363 { 364 ArtifactName: pstr("foo"), 365 Number: 1, 366 SubLines: []SubLine{ 367 { 368 Text: "hello", 369 }, 370 }, 371 }, 372 }, 373 }, 374 })), 375 }, 376 { 377 name: "cookie savable", 378 artifact: &fake.Artifact{ 379 Path: "foo", 380 Content: []byte("hello"), 381 Link: pstr(cookieLink), 382 }, 383 want: render(func() LogArtifactView { 384 lav := view("foo", cookieLink, []LineGroup{ 385 { 386 Start: 1, 387 End: 1, 388 ArtifactName: pstr("foo"), 389 LogLines: []LogLine{ 390 { 391 ArtifactName: pstr("foo"), 392 Number: 1, 393 SubLines: []SubLine{ 394 { 395 Text: "hello", 396 }, 397 }, 398 }, 399 }, 400 }, 401 }) 402 lav.CanSave = true 403 return lav 404 }()), 405 }, 406 { 407 name: "savable", 408 artifact: &fake.Artifact{ 409 Path: "foo", 410 Content: []byte("hello"), 411 Link: pstr(anonLink), 412 }, 413 want: render(func() LogArtifactView { 414 lav := view("foo", anonLink, []LineGroup{ 415 { 416 Start: 1, 417 End: 1, 418 ArtifactName: pstr("foo"), 419 LogLines: []LogLine{ 420 { 421 ArtifactName: pstr("foo"), 422 Number: 1, 423 SubLines: []SubLine{ 424 { 425 Text: "hello", 426 }, 427 }, 428 }, 429 }, 430 }, 431 }) 432 lav.CanSave = true 433 return lav 434 }()), 435 }, 436 { 437 name: "focus", 438 artifact: &fake.Artifact{ 439 Path: "foo", 440 Content: func() []byte { 441 var sb strings.Builder 442 for i := 0; i < 100; i++ { 443 sb.WriteString("word\n") 444 } 445 return []byte(sb.String()) 446 }(), 447 Meta: map[string]string{ 448 focusStart: "20", 449 focusEnd: "35", 450 }, 451 }, 452 want: render(view("foo", fake.NotFound, []LineGroup{ 453 { 454 Start: 0, 455 End: 14, 456 ArtifactName: pstr("foo"), 457 Skip: true, 458 ByteLength: 69, 459 ByteOffset: 0, 460 LogLines: make([]LogLine, 15), 461 }, 462 { 463 Start: 15, 464 End: 40, 465 ArtifactName: pstr("foo"), 466 LogLines: func() []LogLine { 467 var out []LogLine 468 const s = 20 469 const e = 35 470 for i := s - neighborLines; i <= e+neighborLines; i++ { 471 out = append(out, LogLine{ 472 ArtifactName: pstr("foo"), 473 Number: i, 474 Focused: i >= s && i <= e, 475 Clip: i == s, 476 SubLines: []SubLine{ 477 { 478 Text: "word", 479 }, 480 }, 481 }) 482 } 483 return out 484 }(), 485 }, 486 { 487 Start: 40, 488 End: 101, 489 ArtifactName: pstr("foo"), 490 Skip: true, 491 ByteLength: 100*5 - 5*40, 492 ByteOffset: 5 * 40, 493 LogLines: make([]LogLine, 101-40), 494 }, 495 })), 496 }, 497 { 498 name: "auto-focus", 499 rawConfig: json.RawMessage(fmt.Sprintf(`{"highlighter": { 500 "endpoint": "%s", 501 "auto": true 502 }}`, server.URL)), 503 artifact: &fake.Artifact{ 504 Path: "foo", 505 Content: func() []byte { 506 var sb strings.Builder 507 for i := 0; i < 100; i++ { 508 sb.WriteString("word\n") 509 } 510 return []byte(sb.String()) 511 }(), 512 Link: pstr("https://storage.googleapis.com/some-bucket/path/to/foo"), 513 }, 514 highlighter: func() (highlightRequest, int, string) { 515 req := highlightRequest{ 516 URL: "https://storage.googleapis.com/some-bucket/path/to/foo", 517 Pin: true, 518 } 519 resp := highlightResponse{ 520 Min: 20, 521 Max: 35, 522 Pinned: true, 523 } 524 return req, http.StatusOK, marshalHighlightResponse(t, resp) 525 }, 526 want: render(LogArtifactView{ 527 ArtifactName: "foo", 528 ArtifactLink: "https://storage.googleapis.com/some-bucket/path/to/foo", 529 ShowRawLog: true, 530 CanAnalyze: true, 531 ViewAll: true, 532 CanSave: true, 533 LineGroups: []LineGroup{ 534 { 535 Start: 0, 536 End: 14, 537 ArtifactName: pstr("foo"), 538 Skip: true, 539 ByteLength: 69, 540 ByteOffset: 0, 541 LogLines: make([]LogLine, 15), 542 }, 543 { 544 Start: 15, 545 End: 40, 546 ArtifactName: pstr("foo"), 547 LogLines: func() []LogLine { 548 var out []LogLine 549 const s = 20 550 const e = 35 551 for i := s - neighborLines; i <= e+neighborLines; i++ { 552 out = append(out, LogLine{ 553 ArtifactName: pstr("foo"), 554 Number: i, 555 Focused: i >= s && i <= e, 556 Clip: i == s, 557 SubLines: []SubLine{ 558 { 559 Text: "word", 560 }, 561 }, 562 }) 563 } 564 return out 565 }(), 566 }, 567 { 568 Start: 40, 569 End: 101, 570 ArtifactName: pstr("foo"), 571 Skip: true, 572 ByteLength: 100*5 - 5*40, 573 ByteOffset: 5 * 40, 574 LogLines: make([]LogLine, 101-40), 575 }, 576 }, 577 }), 578 }, 579 { 580 name: "missing artifact", 581 want: render(), 582 }, 583 } 584 585 for _, tc := range cases { 586 t.Run(tc.name, func(t *testing.T) { 587 if tc.highlighter != nil { 588 req, code, resp := tc.highlighter() 589 hf = testHighlighter(t, req, code, resp) 590 } else { 591 hf = nil 592 } 593 var arts []api.Artifact 594 if tc.artifact != nil { 595 arts = []api.Artifact{tc.artifact} 596 } 597 const dir = "" 598 const data = "" 599 got := Lens{}.Body(arts, dir, data, tc.rawConfig, prowconfig.Spyglass{}) 600 if diff := cmp.Diff(tc.want, got); diff != "" { 601 t.Errorf("Body() got unexpected diff (-want +got):\n%s", diff) 602 } 603 }) 604 } 605 } 606 607 func TestCallback(t *testing.T) { 608 render := func(groups []*LineGroup) string { 609 return executeTemplate(".", "line groups", groups) 610 } 611 612 cases := []struct { 613 name string 614 artifact *fake.Artifact 615 data string 616 rawConfig json.RawMessage 617 want string 618 wantArtifact func(fake.Artifact) fake.Artifact 619 }{ 620 { 621 name: "empty", 622 data: `{"artifact": "foo"}`, 623 artifact: &fake.Artifact{ 624 Path: "foo", 625 Content: []byte(""), 626 }, 627 want: render([]*LineGroup{ 628 { 629 Start: 1, 630 End: 1, 631 ArtifactName: pstr("foo"), 632 LogLines: []LogLine{ 633 { 634 ArtifactName: pstr("foo"), 635 Number: 1, 636 SubLines: []SubLine{ 637 {}, 638 }, 639 }, 640 }, 641 }, 642 }), 643 }, 644 { 645 name: "single", 646 data: `{ 647 "artifact": "foo", 648 "length": 5 649 650 }`, 651 artifact: &fake.Artifact{ 652 Path: "foo", 653 Content: []byte("hello"), 654 }, 655 want: render([]*LineGroup{ 656 { 657 Start: 1, 658 End: 1, 659 ArtifactName: pstr("foo"), 660 LogLines: []LogLine{ 661 { 662 ArtifactName: pstr("foo"), 663 Number: 1, 664 SubLines: []SubLine{ 665 { 666 Text: "hello", 667 }, 668 }, 669 }, 670 }, 671 }, 672 }), 673 }, 674 { 675 name: "multiple", 676 data: `{ 677 "artifact": "foo", 678 "length": 11 679 680 }`, 681 artifact: &fake.Artifact{ 682 Path: "foo", 683 Content: []byte("hello\nworld"), 684 }, 685 want: render([]*LineGroup{ 686 { 687 Start: 1, 688 End: 2, 689 ArtifactName: pstr("foo"), 690 LogLines: []LogLine{ 691 { 692 ArtifactName: pstr("foo"), 693 Number: 1, 694 SubLines: []SubLine{ 695 { 696 Text: "hello", 697 }, 698 }, 699 }, 700 { 701 ArtifactName: pstr("foo"), 702 Number: 2, 703 SubLines: []SubLine{ 704 { 705 Text: "world", 706 }, 707 }, 708 }, 709 }, 710 }, 711 }), 712 }, 713 { 714 name: "top", 715 data: `{ 716 "artifact": "foo", 717 "top": 3, 718 "length": 400 719 }`, 720 artifact: &fake.Artifact{ 721 Path: "foo", 722 Content: func() []byte { 723 var sb strings.Builder 724 for i := 0; i < 100; i++ { 725 sb.WriteString("word\n") 726 } 727 return []byte(sb.String()) 728 }(), 729 }, 730 want: render([]*LineGroup{ 731 { 732 Start: 1, 733 End: 3, 734 ArtifactName: pstr("foo"), 735 LogLines: []LogLine{ 736 { 737 ArtifactName: pstr("foo"), 738 Number: 1, 739 SubLines: []SubLine{ 740 { 741 Text: "word", 742 }, 743 }, 744 }, 745 { 746 ArtifactName: pstr("foo"), 747 Number: 2, 748 SubLines: []SubLine{ 749 { 750 Text: "word", 751 }, 752 }, 753 }, 754 { 755 ArtifactName: pstr("foo"), 756 Number: 3, 757 SubLines: []SubLine{ 758 { 759 Text: "word", 760 }, 761 }, 762 }, 763 }, 764 }, 765 { 766 Start: 3, 767 End: 81, 768 ArtifactName: pstr("foo"), 769 Skip: true, 770 ByteLength: 385, 771 ByteOffset: 15, 772 LogLines: make([]LogLine, 77), 773 }, 774 }), 775 }, 776 { 777 name: "bottom", 778 data: `{ 779 "artifact": "foo", 780 "bottom": 3, 781 "length": 400 782 }`, 783 artifact: &fake.Artifact{ 784 Path: "foo", 785 Content: func() []byte { 786 var sb strings.Builder 787 for i := 0; i < 100; i++ { 788 sb.WriteString("word\n") 789 } 790 return []byte(sb.String()) 791 }(), 792 }, 793 want: render([]*LineGroup{ 794 { 795 Start: 0, 796 End: 78, 797 ArtifactName: pstr("foo"), 798 Skip: true, 799 ByteLength: 389, 800 ByteOffset: 0, 801 LogLines: make([]LogLine, 78), 802 }, 803 { 804 Start: 78, 805 End: 80, 806 ArtifactName: pstr("foo"), 807 LogLines: []LogLine{ 808 { 809 ArtifactName: pstr("foo"), 810 Number: 79, 811 SubLines: []SubLine{ 812 { 813 Text: "word", 814 }, 815 }, 816 }, 817 { 818 ArtifactName: pstr("foo"), 819 Number: 80, 820 SubLines: []SubLine{ 821 { 822 Text: "word", 823 }, 824 }, 825 }, 826 { 827 ArtifactName: pstr("foo"), 828 Number: 81, 829 SubLines: []SubLine{ 830 { 831 Text: "", 832 }, 833 }, 834 }, 835 }, 836 }, 837 }), 838 }, 839 { 840 name: "full", 841 data: `{ 842 "artifact": "foo", 843 "length": 400 844 }`, 845 artifact: &fake.Artifact{ 846 Path: "foo", 847 Content: func() []byte { 848 var sb strings.Builder 849 for i := 0; i < 100; i++ { 850 sb.WriteString("word\n") 851 } 852 return []byte(sb.String()) 853 }(), 854 }, 855 want: render([]*LineGroup{ 856 { 857 Start: 0, 858 End: 81, 859 ArtifactName: pstr("foo"), 860 LogLines: func() []LogLine { 861 out := make([]LogLine, 0, 81) 862 for i := 0; i < 80; i++ { 863 out = append(out, LogLine{ 864 ArtifactName: pstr("foo"), 865 Number: i + 1, 866 SubLines: []SubLine{ 867 { 868 Text: "word", 869 }, 870 }, 871 }) 872 } 873 out = append(out, LogLine{ 874 ArtifactName: pstr("foo"), 875 Number: 81, 876 SubLines: []SubLine{ 877 { 878 Text: "", 879 }, 880 }, 881 }) 882 return out 883 }(), 884 }, 885 }), 886 }, 887 { 888 name: "save", 889 data: `{ 890 "artifact": "foo", 891 "startLine": 7, 892 "saveEnd": 20 893 }`, 894 artifact: &fake.Artifact{ 895 Path: "foo", 896 Content: []byte("irrelevant"), 897 }, 898 want: "", 899 wantArtifact: func(a fake.Artifact) fake.Artifact { 900 a.Meta = map[string]string{ 901 focusStart: "7", 902 focusEnd: "20", 903 } 904 return a 905 }, 906 }, 907 { 908 name: "highlight", 909 data: `{ 910 "artifact": "bar", 911 "analyze": true 912 }`, 913 artifact: &fake.Artifact{ 914 Path: "bar", 915 Content: []byte("irrelevant"), 916 }, 917 want: marshalHighlightResponse(t, highlightResponse{Error: errNoHighlighter.Error()}), 918 }, 919 { 920 name: "bad json", 921 want: failedUnmarshal, 922 }, 923 { 924 name: "missing artifact", 925 data: `{"artifact": "foo"}`, 926 want: fmt.Sprintf(missingArtifact, "foo"), 927 }, 928 } 929 930 for _, tc := range cases { 931 t.Run(tc.name, func(t *testing.T) { 932 var arts []api.Artifact 933 if tc.artifact != nil { 934 arts = []api.Artifact{tc.artifact} 935 } 936 got := Lens{}.Callback(arts, "", tc.data, tc.rawConfig, prowconfig.Spyglass{}) 937 if diff := cmp.Diff(tc.want, got); diff != "" { 938 t.Errorf("Callback() got unexpected diff (-want +got):\n%s", diff) 939 } 940 941 if tc.wantArtifact != nil { 942 want := tc.wantArtifact(*tc.artifact) 943 if diff := cmp.Diff(&want, tc.artifact); diff != "" { 944 t.Errorf("Callback() got unexpected artifact diff (-want +got):\n%s", diff) 945 } 946 } 947 }) 948 } 949 } 950 951 func marshalHighlightResponse(t *testing.T, hr highlightResponse) string { 952 b, err := json.Marshal(hr) 953 if err != nil { 954 t.Fatalf("Failed to marshal response %#v: %v", hr, err) 955 } 956 return string(b) 957 } 958 959 func TestAnalyzeArtifact(t *testing.T) { 960 961 basicResponse := highlightResponse{ 962 Min: 1, 963 Max: 2, 964 Link: "hello", 965 Pinned: true, 966 } 967 968 cases := []struct { 969 name string 970 art api.Artifact 971 high *highlightConfig // endpoint replaced with fake 972 wantReq *highlightRequest 973 code int 974 response string 975 want *highlightResponse 976 err bool 977 }{ 978 { 979 name: "unconfigured", 980 err: true, 981 }, 982 { 983 name: "unsavable link", 984 high: &highlightConfig{}, 985 art: &fake.Artifact{}, 986 err: true, 987 }, 988 { 989 name: "unparseable link", 990 high: &highlightConfig{}, 991 art: &fake.Artifact{ 992 Link: pstr("bad::%\x00:://" + pkgio.GSAnonHost), 993 }, 994 err: true, 995 }, 996 { 997 name: "basic", 998 high: &highlightConfig{}, 999 art: &fake.Artifact{ 1000 Link: pstr("https://storage.googleapis.com/bucket/obj"), 1001 }, 1002 wantReq: &highlightRequest{ 1003 URL: "https://storage.googleapis.com/bucket/obj", 1004 }, 1005 code: http.StatusOK, 1006 response: marshalHighlightResponse(t, basicResponse), 1007 want: &basicResponse, 1008 }, 1009 { 1010 name: "pin", 1011 high: &highlightConfig{ 1012 Pin: true, 1013 Overwrite: true, 1014 }, 1015 art: &fake.Artifact{ 1016 Link: pstr("https://storage.googleapis.com/bucket/obj"), 1017 }, 1018 wantReq: &highlightRequest{ 1019 URL: "https://storage.googleapis.com/bucket/obj", 1020 Overwrite: true, 1021 Pin: true, 1022 }, 1023 code: http.StatusOK, 1024 response: marshalHighlightResponse(t, basicResponse), 1025 want: &basicResponse, 1026 }, 1027 { 1028 name: "auto pin", 1029 high: &highlightConfig{ 1030 Auto: true, 1031 Overwrite: true, 1032 }, 1033 art: &fake.Artifact{ 1034 Link: pstr("https://storage.googleapis.com/bucket/obj"), 1035 }, 1036 wantReq: &highlightRequest{ 1037 URL: "https://storage.googleapis.com/bucket/obj", 1038 Overwrite: true, 1039 Pin: true, 1040 }, 1041 code: http.StatusOK, 1042 response: marshalHighlightResponse(t, basicResponse), 1043 want: &basicResponse, 1044 }, 1045 { 1046 name: "bad status", 1047 high: &highlightConfig{}, 1048 art: &fake.Artifact{ 1049 Link: pstr("https://storage.googleapis.com/bucket/obj"), 1050 }, 1051 wantReq: &highlightRequest{ 1052 URL: "https://storage.googleapis.com/bucket/obj", 1053 }, 1054 code: http.StatusNotFound, 1055 response: marshalHighlightResponse(t, basicResponse), 1056 err: true, 1057 }, 1058 { 1059 name: "bad response", 1060 high: &highlightConfig{}, 1061 art: &fake.Artifact{ 1062 Link: pstr("https://storage.googleapis.com/bucket/obj"), 1063 }, 1064 wantReq: &highlightRequest{ 1065 URL: "https://storage.googleapis.com/bucket/obj", 1066 }, 1067 code: http.StatusOK, 1068 response: "this is [ not a json object", 1069 err: true, 1070 }, 1071 } 1072 1073 var hf http.HandlerFunc 1074 1075 server := httptest.NewServer(&hf) 1076 defer server.Close() 1077 1078 for _, tc := range cases { 1079 t.Run(tc.name, func(t *testing.T) { 1080 if tc.wantReq != nil { 1081 hf = testHighlighter(t, *tc.wantReq, tc.code, tc.response) 1082 } else { 1083 hf = nil 1084 } 1085 if tc.high != nil { 1086 tc.high.Endpoint = server.URL 1087 } 1088 conf := parsedConfig{ 1089 highlighter: tc.high, 1090 } 1091 got, err := analyzeArtifact(tc.art, &conf) 1092 switch { 1093 case err != nil: 1094 if !tc.err { 1095 t.Errorf("analyzeArtifact() got unexpected error: %v", err) 1096 } 1097 case tc.err: 1098 t.Errorf("analyzeArtifact wanted err, got %v", got) 1099 default: 1100 if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(highlightResponse{})); diff != "" { 1101 t.Errorf("analyzeArtifact() got unexpected diff (-want +got):\n%s", diff) 1102 } 1103 } 1104 }) 1105 } 1106 } 1107 1108 func testHighlighter(t *testing.T, wantReq highlightRequest, code int, response string) http.HandlerFunc { 1109 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1110 t.Helper() 1111 buf, err := stdio.ReadAll(r.Body) 1112 if err != nil { 1113 t.Fatalf("Failed to read body: %v", err) 1114 } 1115 1116 if err := r.Body.Close(); err != nil { 1117 t.Fatalf("Failed to close request: %v", err) 1118 } 1119 1120 var gotReq highlightRequest 1121 if err := json.Unmarshal(buf, &gotReq); err != nil { 1122 t.Fatalf("Failed to parse request: %v", err) 1123 } 1124 1125 if diff := cmp.Diff(wantReq, gotReq, cmp.AllowUnexported(highlightRequest{})); diff != "" { 1126 t.Fatalf("Received unexpected request (-want +got):\n%s", diff) 1127 } 1128 1129 w.WriteHeader(code) 1130 if _, err := w.Write([]byte(response)); err != nil { 1131 t.Fatalf("failed to write %q: %v", response, err) 1132 } 1133 }) 1134 } 1135 1136 func BenchmarkHighlightLines(b *testing.B) { 1137 lorem := []string{ 1138 "Lorem ipsum dolor sit amet", 1139 "consectetur adipiscing elit", 1140 "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua", 1141 "Ut enim ad minim veniam", 1142 "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat", 1143 "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur", 1144 "Excepteur sint occaecat cupidatat non proident", 1145 "sunt in culpa qui officia deserunt mollit anim id est laborum", 1146 } 1147 art := "fake-artifact" 1148 b.Run("HighlightLines", func(b *testing.B) { 1149 _ = highlightLines(lorem, 0, &art, defaultErrRE, defaultHighlightLineLengthMax) 1150 }) 1151 }