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  }