github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/deck/pr_history_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 main
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/url"
    23  	"reflect"
    24  	"strings"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/fsouza/fake-gcs-server/fakestorage"
    29  
    30  	"sigs.k8s.io/prow/pkg/io"
    31  
    32  	"github.com/google/go-cmp/cmp"
    33  	"k8s.io/apimachinery/pkg/util/sets"
    34  
    35  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    36  	"sigs.k8s.io/prow/pkg/config"
    37  	"sigs.k8s.io/prow/pkg/github"
    38  	"sigs.k8s.io/prow/pkg/github/fakegithub"
    39  	"sigs.k8s.io/prow/pkg/io/providers"
    40  )
    41  
    42  type fakeBucket struct {
    43  	name            string
    44  	storageProvider string
    45  	objects         map[string]string
    46  }
    47  
    48  func (bucket fakeBucket) getName() string {
    49  	return bucket.name
    50  }
    51  
    52  func (bucket fakeBucket) getStorageProvider() string {
    53  	return bucket.storageProvider
    54  }
    55  
    56  func (bucket fakeBucket) listSubDirs(_ context.Context, prefix string) ([]string, error) {
    57  	dirs := sets.Set[string]{}
    58  	for k := range bucket.objects {
    59  		if !strings.HasPrefix(k, prefix) {
    60  			continue
    61  		}
    62  		suffix := strings.TrimPrefix(k, prefix)
    63  		dir := strings.Split(suffix, "/")[0]
    64  		dirs.Insert(dir)
    65  	}
    66  	return sets.List(dirs), nil
    67  }
    68  
    69  func (bucket fakeBucket) listAll(_ context.Context, prefix string) ([]string, error) {
    70  	keys := []string{}
    71  	for k := range bucket.objects {
    72  		if !strings.HasPrefix(k, prefix) {
    73  			continue
    74  		}
    75  		keys = append(keys, k)
    76  	}
    77  	return keys, nil
    78  }
    79  
    80  func (bucket fakeBucket) readObject(_ context.Context, key string) ([]byte, error) {
    81  	if obj, ok := bucket.objects[key]; ok {
    82  		return []byte(obj), nil
    83  	}
    84  	return []byte{}, fmt.Errorf("object %s not found", key)
    85  }
    86  
    87  func TestUpdateCommitData(t *testing.T) {
    88  	cases := []struct {
    89  		name      string
    90  		hash      string
    91  		buildTime time.Time
    92  		width     int
    93  		before    map[string]*commitData
    94  		after     map[string]*commitData
    95  	}{
    96  		{
    97  			name:      "new commit",
    98  			hash:      "d0c3cd182cffb3e722b14322fd1ca854a8bf62b0",
    99  			width:     1,
   100  			buildTime: time.Unix(1543534799, 0),
   101  			before:    make(map[string]*commitData),
   102  			after: map[string]*commitData{
   103  				"d0c3cd182cffb3e722b14322fd1ca854a8bf62b0": {
   104  					HashPrefix: "d0c3cd1",
   105  					MaxWidth:   1,
   106  					Link:       "https://github.com/kubernetes/test-infra/commit/d0c3cd182cffb3e722b14322fd1ca854a8bf62b0",
   107  					latest:     time.Unix(1543534799, 0),
   108  				},
   109  			},
   110  		},
   111  		{
   112  			name:      "update existing commit",
   113  			hash:      "d0c3cd182cffb3e722b14322fd1ca854a8bf62b0",
   114  			width:     3,
   115  			buildTime: time.Unix(320630400, 0),
   116  			before: map[string]*commitData{
   117  				"d0c3cd182cffb3e722b14322fd1ca854a8bf62b0": {
   118  					HashPrefix: "d0c3cd1",
   119  					MaxWidth:   5,
   120  					Link:       "https://github.com/kubernetes/test-infra/commit/d0c3cd182cffb3e722b14322fd1ca854a8bf62b0",
   121  					latest:     time.Unix(0, 0),
   122  				},
   123  			},
   124  			after: map[string]*commitData{
   125  				"d0c3cd182cffb3e722b14322fd1ca854a8bf62b0": {
   126  					HashPrefix: "d0c3cd1",
   127  					MaxWidth:   5,
   128  					Link:       "https://github.com/kubernetes/test-infra/commit/d0c3cd182cffb3e722b14322fd1ca854a8bf62b0",
   129  					latest:     time.Unix(320630400, 0),
   130  				},
   131  			},
   132  		},
   133  		{
   134  			name:   "unknown commit has no link",
   135  			hash:   "unknown",
   136  			width:  1,
   137  			before: make(map[string]*commitData),
   138  			after: map[string]*commitData{
   139  				"unknown": {
   140  					HashPrefix: "unknown",
   141  					MaxWidth:   1,
   142  					Link:       "",
   143  				},
   144  			},
   145  		},
   146  	}
   147  	org := "kubernetes"
   148  	repo := "test-infra"
   149  	for _, tc := range cases {
   150  		updateCommitData(tc.before, "github.com", org, repo, tc.hash, tc.buildTime, tc.width)
   151  		for hash, expCommit := range tc.after {
   152  			if commit, ok := tc.before[hash]; ok {
   153  				if commit.HashPrefix != expCommit.HashPrefix {
   154  					t.Errorf("%s: expected commit hash prefix to be %s, got %s", tc.name, expCommit.HashPrefix, commit.HashPrefix)
   155  				}
   156  				if commit.Link != expCommit.Link {
   157  					t.Errorf("%s: expected commit link to be %s, got %s", tc.name, expCommit.Link, commit.Link)
   158  				}
   159  				if commit.MaxWidth != expCommit.MaxWidth {
   160  					t.Errorf("%s: expected commit width to be %d, got %d", tc.name, expCommit.MaxWidth, commit.MaxWidth)
   161  				}
   162  				if commit.latest != expCommit.latest {
   163  					t.Errorf("%s: expected commit time to be %v, got %v", tc.name, expCommit.latest, commit.latest)
   164  				}
   165  			} else {
   166  				t.Errorf("%s: expected commit %s not found", tc.name, hash)
   167  			}
   168  		}
   169  	}
   170  }
   171  
   172  func TestGetPullCommitHash(t *testing.T) {
   173  	cases := []struct {
   174  		pull       string
   175  		commitHash string
   176  		expErr     bool
   177  	}{
   178  		{
   179  			pull:       "main:4fe6d226e0455ef3d16c1f639a4010d699d0d097,21354:6cf03d53a14f6287d2175b0e9f3fbb31d91981a7",
   180  			commitHash: "6cf03d53a14f6287d2175b0e9f3fbb31d91981a7",
   181  		},
   182  		{
   183  			pull:       "release45-v8.0:5b30685f6bbf7a0bfef3fa8f2ebe2626ec1df391,54884:d1e309d8d10388000a34b1f705fd78c648ea5faa",
   184  			commitHash: "d1e309d8d10388000a34b1f705fd78c648ea5faa",
   185  		},
   186  		{
   187  			pull:   "main:6c1db48d6911675873b25457dbe61adca0d428a0,pullre:4905771e4f06c00385d7b1ac3c6de76f173e0212",
   188  			expErr: true,
   189  		},
   190  		{
   191  			pull:   "23545",
   192  			expErr: true,
   193  		},
   194  		{
   195  			pull:   "main:6c1db48d6911675873b25457dbe61adca0d428a0,12354:548461",
   196  			expErr: true,
   197  		},
   198  		{
   199  			pull:   "main:6c1db48d6,12354:e3e9d3eaa3a43f0a4fac47eccd379f077bee6789",
   200  			expErr: true,
   201  		},
   202  	}
   203  
   204  	for _, tc := range cases {
   205  		commitHash, err := getPullCommitHash(tc.pull)
   206  		if (err != nil) != tc.expErr {
   207  			t.Errorf("%q: unexpected error: %v", tc.pull, err)
   208  			continue
   209  		}
   210  		if commitHash != tc.commitHash {
   211  			t.Errorf("%s: expected commit hash to be '%s', got '%s'", tc.pull, tc.commitHash, commitHash)
   212  		}
   213  	}
   214  }
   215  
   216  func TestParsePullURL(t *testing.T) {
   217  	cases := []struct {
   218  		name   string
   219  		addr   string
   220  		org    string
   221  		repo   string
   222  		pr     int
   223  		expErr bool
   224  	}{
   225  		{
   226  			name: "simple org/repo",
   227  			addr: "https://prow.k8s.io/pr-history?org=kubernetes&repo=test-infra&pr=10169",
   228  			org:  "kubernetes",
   229  			repo: "test-infra",
   230  			pr:   10169,
   231  		},
   232  		{
   233  			name: "Gerrit org/repo",
   234  			addr: "https://prow.k8s.io/pr-history?org=http://theponyapi.com&repo=test/ponies&pr=12345",
   235  			org:  "http://theponyapi.com",
   236  			repo: "test/ponies",
   237  			pr:   12345,
   238  		},
   239  		{
   240  			name:   "PR needs to be an int",
   241  			addr:   "https://prow.k8s.io/pr-history?org=kubernetes&repo=test-infra&pr=alpha",
   242  			expErr: true,
   243  		},
   244  		{
   245  			name:   "missing org",
   246  			addr:   "https://prow.k8s.io/pr-history?repo=test-infra&pr=10169",
   247  			expErr: true,
   248  		},
   249  		{
   250  			name:   "missing repo",
   251  			addr:   "https://prow.k8s.io/pr-history?org=kubernetes&pr=10169",
   252  			expErr: true,
   253  		},
   254  		{
   255  			name:   "missing pr",
   256  			addr:   "https://prow.k8s.io/pr-history?org=kubernetes&repo=test-infra",
   257  			expErr: true,
   258  		},
   259  	}
   260  	for _, tc := range cases {
   261  		u, err := url.Parse(tc.addr)
   262  		if err != nil {
   263  			t.Errorf("bad test URL %s: %v", tc.addr, err)
   264  			continue
   265  		}
   266  		org, repo, pr, err := parsePullURL(u)
   267  		if (err != nil) != tc.expErr {
   268  			t.Errorf("%q: unexpected error: %v", tc.name, err)
   269  		}
   270  		if org != tc.org || repo != tc.repo || pr != tc.pr {
   271  			t.Errorf("%q: expected %s, %s, %d; got %s, %s, %d", tc.name, tc.org, tc.repo, tc.pr, org, repo, pr)
   272  		}
   273  	}
   274  }
   275  
   276  var testBucket = fakeBucket{
   277  	name:            "chum-bucket",
   278  	storageProvider: providers.GS,
   279  	objects: map[string]string{
   280  		"pr-logs/pull/123/build-snowman/456/started.json": `{
   281  			"timestamp": 55555
   282  		}`,
   283  		"pr-logs/pull/123/build-snowman/456/finished.json": `{
   284  			"timestamp": 66666,
   285  			"result":    "SUCCESS",
   286  			"revision":  "1244ee66517bbe603d899bbd24458ebc0e185fd9"
   287  		}`,
   288  		"pr-logs/pull/123/build-snowman/789/started.json": `{
   289  			"timestamp": 98765,
   290  			"pull": "master:d0c3cd182cffb3e722b14322fd1ca854a8bf62b0,69848:bbdebedaf24c03f9e2eeb88e8ea4bb10c9e1fbfc"
   291  		}`,
   292  		"pr-logs/pull/765/eat-bread/999/started.json": `{
   293  			"timestamp": 12345,
   294  			"pull": "not-master:21ebe05079a1aeb5f6dae23a2d8c106b4af8c363,12345:52252bcc81712c96940fca1d3c913dd76af3d2a2"
   295  		}`,
   296  	},
   297  }
   298  
   299  func TestListJobBuilds(t *testing.T) {
   300  	jobPrefixes := []string{"pr-logs/pull/123/build-snowman/", "pr-logs/pull/765/eat-bread/"}
   301  	expected := map[string]sets.Set[string]{
   302  		"build-snowman": {"456": {}, "789": {}},
   303  		"eat-bread":     {"999": {}},
   304  	}
   305  	jobs := listJobBuilds(context.Background(), testBucket, jobPrefixes)
   306  	if len(jobs) != len(expected) {
   307  		t.Errorf("expected %d jobs, got %d", len(expected), len(jobs))
   308  	}
   309  	for _, job := range jobs {
   310  		if expBuilds, ok := expected[job.name]; ok {
   311  			if len(job.buildPrefixes) != len(expBuilds) {
   312  				t.Errorf("expected %d builds for %q, found %d", len(expBuilds), job.name, len(job.buildPrefixes))
   313  			}
   314  			for _, build := range job.buildPrefixes {
   315  				if !expBuilds.Has(build) {
   316  					t.Errorf("found unexpected build for %q: %q", job.name, build)
   317  				}
   318  			}
   319  		} else {
   320  			t.Errorf("found unexpected job %q", job.name)
   321  		}
   322  	}
   323  }
   324  
   325  func TestGetPRBuildData(t *testing.T) {
   326  	jobs := []jobBuilds{
   327  		{
   328  			name: "build-snowman",
   329  			buildPrefixes: []string{
   330  				"pr-logs/pull/123/build-snowman/456",
   331  				"pr-logs/pull/123/build-snowman/789",
   332  			},
   333  		},
   334  		{
   335  			name: "eat-bread",
   336  			buildPrefixes: []string{
   337  				"pr-logs/pull/765/eat-bread/999",
   338  			},
   339  		},
   340  	}
   341  	expected := map[string]struct {
   342  		fixedDuration bool
   343  		buildData     buildData
   344  	}{
   345  		"pr-logs/pull/123/build-snowman/456": {
   346  			fixedDuration: true,
   347  			buildData: buildData{
   348  				prefix:       "pr-logs/pull/123/build-snowman/456",
   349  				jobName:      "build-snowman",
   350  				index:        0,
   351  				ID:           "456",
   352  				SpyglassLink: "/view/gs/chum-bucket/pr-logs/pull/123/build-snowman/456",
   353  				Started:      time.Unix(55555, 0),
   354  				Duration:     time.Unix(66666, 0).Sub(time.Unix(55555, 0)),
   355  				Result:       "SUCCESS",
   356  				commitHash:   "1244ee66517bbe603d899bbd24458ebc0e185fd9",
   357  			},
   358  		},
   359  		"pr-logs/pull/123/build-snowman/789": {
   360  			buildData: buildData{
   361  				prefix:       "pr-logs/pull/123/build-snowman/789",
   362  				jobName:      "build-snowman",
   363  				index:        1,
   364  				ID:           "789",
   365  				SpyglassLink: "/view/gs/chum-bucket/pr-logs/pull/123/build-snowman/789",
   366  				Started:      time.Unix(98765, 0),
   367  				Result:       "Pending",
   368  				commitHash:   "bbdebedaf24c03f9e2eeb88e8ea4bb10c9e1fbfc",
   369  			},
   370  		},
   371  		"pr-logs/pull/765/eat-bread/999": {
   372  			buildData: buildData{
   373  				prefix:       "pr-logs/pull/765/eat-bread/999",
   374  				jobName:      "eat-bread",
   375  				index:        0,
   376  				ID:           "999",
   377  				SpyglassLink: "/view/gs/chum-bucket/pr-logs/pull/765/eat-bread/999",
   378  				Started:      time.Unix(12345, 0),
   379  				Result:       "Pending",
   380  				commitHash:   "52252bcc81712c96940fca1d3c913dd76af3d2a2",
   381  			},
   382  		},
   383  	}
   384  	builds := getPRBuildData(context.Background(), testBucket, jobs)
   385  	if len(builds) != len(expected) {
   386  		t.Errorf("expected %d builds, found %d", len(expected), len(builds))
   387  	}
   388  	cmpOption := cmp.AllowUnexported(buildData{})
   389  	for _, build := range builds {
   390  		if exp, ok := expected[build.prefix]; ok {
   391  			if !exp.fixedDuration {
   392  				build.Duration = 0
   393  			}
   394  
   395  			if diff := cmp.Diff(build, exp.buildData, cmpOption); diff != "" {
   396  				t.Errorf("build %s mismatch (-got, +want):\n%s", build.prefix, diff)
   397  			}
   398  		} else {
   399  			t.Errorf("found unexpected build %s", build.prefix)
   400  		}
   401  	}
   402  }
   403  
   404  func TestGetGCSDirsForPR(t *testing.T) {
   405  	cases := []struct {
   406  		name     string
   407  		expected map[string][]string
   408  		config   *config.Config
   409  		org      string
   410  		repo     string
   411  		pr       int
   412  		expErr   bool
   413  	}{
   414  		{
   415  			name:   "no presubmits",
   416  			org:    "kubernetes",
   417  			repo:   "fizzbuzz",
   418  			pr:     123,
   419  			config: &config.Config{},
   420  			expErr: true,
   421  		},
   422  		{
   423  			name: "multiple buckets",
   424  			expected: map[string][]string{
   425  				"gs://chum-bucket": {
   426  					"pr-logs/pull/prow/123/",
   427  				},
   428  				"gs://krusty-krab": {
   429  					"pr-logs/pull/prow/123/",
   430  				},
   431  			},
   432  			org:  "kubernetes",
   433  			repo: "prow", // someday
   434  			pr:   123,
   435  			config: &config.Config{
   436  				ProwConfig: config.ProwConfig{
   437  					Plank: config.Plank{
   438  						DefaultDecorationConfigs: config.DefaultDecorationMapToSliceTesting(
   439  							map[string]*prowapi.DecorationConfig{
   440  								"*": {
   441  									GCSConfiguration: &prowapi.GCSConfiguration{
   442  										Bucket:       "krusty-krab",
   443  										PathStrategy: "legacy",
   444  										DefaultOrg:   "kubernetes",
   445  										DefaultRepo:  "kubernetes",
   446  									},
   447  								},
   448  							}),
   449  					},
   450  				},
   451  				JobConfig: config.JobConfig{
   452  					PresubmitsStatic: map[string][]config.Presubmit{
   453  						"kubernetes/prow": {
   454  							{
   455  								JobBase: config.JobBase{
   456  									Name: "fum-is-chum",
   457  									UtilityConfig: config.UtilityConfig{
   458  										DecorationConfig: &prowapi.DecorationConfig{
   459  											GCSConfiguration: &prowapi.GCSConfiguration{
   460  												Bucket:       "chum-bucket",
   461  												PathStrategy: "legacy",
   462  												DefaultOrg:   "kubernetes",
   463  												DefaultRepo:  "kubernetes",
   464  											},
   465  										},
   466  									},
   467  								},
   468  							},
   469  							{
   470  								JobBase: config.JobBase{
   471  									Name: "protect-formula",
   472  									// undecorated
   473  								},
   474  							},
   475  						},
   476  					},
   477  				},
   478  			},
   479  		},
   480  	}
   481  	for _, tc := range cases {
   482  		gitHubClient := fakegithub.NewFakeClient()
   483  		gitHubClient.PullRequests = map[int]*github.PullRequest{
   484  			123: {Number: 123},
   485  		}
   486  		toSearch, err := getStorageDirsForPR(tc.config, gitHubClient, nil, tc.org, tc.repo, "", tc.pr)
   487  		if (err != nil) != tc.expErr {
   488  			t.Errorf("%s: unexpected error %v", tc.name, err)
   489  		}
   490  		for bucket, expDirs := range tc.expected {
   491  			if dirs, ok := toSearch[bucket]; ok {
   492  				if len(dirs) != len(expDirs) {
   493  					t.Errorf("expected to find %d dirs in bucket %s, found %d", len(expDirs), bucket, len(dirs))
   494  				}
   495  				for _, expDir := range tc.expected[bucket] {
   496  					if !dirs.Has(expDir) {
   497  						t.Errorf("couldn't find expected dir %s in bucket %s", expDir, bucket)
   498  					}
   499  				}
   500  			} else {
   501  				t.Errorf("expected to find %d dirs in bucket %s, found none", len(expDirs), bucket)
   502  			}
   503  		}
   504  	}
   505  }
   506  
   507  func Test_getPRHistory(t *testing.T) {
   508  	c := &config.Config{
   509  		JobConfig: config.JobConfig{
   510  			PresubmitsStatic: map[string][]config.Presubmit{
   511  				"kubernetes/test-infra": {
   512  					{
   513  						JobBase: config.JobBase{
   514  							Name: "pull-test-infra-bazel",
   515  							UtilityConfig: config.UtilityConfig{
   516  								DecorationConfig: &prowapi.DecorationConfig{
   517  									GCSConfiguration: &prowapi.GCSConfiguration{
   518  										Bucket:       "kubernetes-jenkins",
   519  										PathStrategy: prowapi.PathStrategyLegacy,
   520  										DefaultOrg:   "kubernetes",
   521  									},
   522  								},
   523  							},
   524  						},
   525  					},
   526  					{
   527  						JobBase: config.JobBase{
   528  							Name: "pull-test-infra-yamllint",
   529  						},
   530  					},
   531  				},
   532  			},
   533  		},
   534  		ProwConfig: config.ProwConfig{
   535  			Plank: config.Plank{
   536  				DefaultDecorationConfigs: config.DefaultDecorationMapToSliceTesting(
   537  					map[string]*prowapi.DecorationConfig{
   538  						"*": {
   539  							GCSConfiguration: &prowapi.GCSConfiguration{
   540  								Bucket:       "gs://kubernetes-jenkins",
   541  								PathStrategy: prowapi.PathStrategyLegacy,
   542  								DefaultOrg:   "kubernetes",
   543  							},
   544  						},
   545  					}),
   546  			},
   547  			Deck: config.Deck{
   548  				AllKnownStorageBuckets: sets.New[string]("kubernetes-jenkins"),
   549  			},
   550  		},
   551  	}
   552  	objects := []fakestorage.Object{
   553  		{
   554  			BucketName: "kubernetes-jenkins",
   555  			Name:       "pr-logs/pull/test-infra/17183/pull-test-infra-bazel/1254406011708510210/started.json",
   556  			Content:    []byte("{\"timestamp\": 1587908709,\"pull\": \"17183\",\"repos\": {\"kubernetes/test-infra\": \"master:48192e9a938ed25edb646de2ee9b4ec096c02732,17183:664ba002bc2155e7438b810a1bb7473c55dc1c6a\"},\"metadata\": {\"resultstore\": \"https://source.cloud.google.com/results/invocations/8edcebc7-11f3-4c4e-a7c3-cae6d26bd117/targets/test\"},\"repo-version\": \"a31d10b2924182638acad0f4b759f53e73b5f817\",\"Pending\": false}"),
   557  		},
   558  		{
   559  			BucketName: "kubernetes-jenkins",
   560  			Name:       "pr-logs/pull/test-infra/17183/pull-test-infra-bazel/1254406011708510210/finished.json",
   561  			Content:    []byte("{\"timestamp\": 1587909145,\"passed\": true,\"result\": \"SUCCESS\",\"revision\": \"664ba002bc2155e7438b810a1bb7473c55dc1c6a\"}"),
   562  		},
   563  		{
   564  			BucketName: "kubernetes-jenkins",
   565  			Name:       "pr-logs/pull/test-infra/17183/pull-test-infra-yamllint/1254406011708510208/started.json",
   566  			Content:    []byte("{\"timestamp\": 1587908749,\"pull\": \"17183\",\"repos\": {\"kubernetes/test-infra\": \"master:48192e9a938ed25edb646de2ee9b4ec096c02732,17183:664ba002bc2155e7438b810a1bb7473c55dc1c6a\"},\"metadata\": {\"resultstore\": \"https://source.cloud.google.com/results/invocations/af70141d-0990-4e63-9ebf-db874391865e/targets/test\"},\"repo-version\": \"a31d10b2924182638acad0f4b759f53e73b5f817\",\"Pending\": false}"),
   567  		},
   568  		{
   569  			BucketName: "kubernetes-jenkins",
   570  			Name:       "pr-logs/pull/test-infra/17183/pull-test-infra-yamllint/1254406011708510208/finished.json",
   571  			Content:    []byte("{\"timestamp\": 1587908767,\"passed\": true,\"result\": \"SUCCESS\",\"revision\": \"664ba002bc2155e7438b810a1bb7473c55dc1c6a\"}"),
   572  		},
   573  	}
   574  	gcsServer := fakestorage.NewServer(objects)
   575  	defer gcsServer.Stop()
   576  
   577  	fakeGCSClient := gcsServer.Client()
   578  
   579  	wantedPRHistory := prHistoryTemplate{
   580  		Link: "https://github.com/kubernetes/test-infra/pull/17183",
   581  		Name: "kubernetes/test-infra #17183",
   582  		Jobs: []prJobData{
   583  			{
   584  				Name: "pull-test-infra-bazel",
   585  				Link: "/job-history/gs/kubernetes-jenkins/pr-logs/directory/pull-test-infra-bazel",
   586  				Builds: []buildData{
   587  					{
   588  						index:        0,
   589  						jobName:      "pull-test-infra-bazel",
   590  						prefix:       "pr-logs/pull/test-infra/17183/pull-test-infra-bazel/1254406011708510210/",
   591  						SpyglassLink: "/view/gs/kubernetes-jenkins/pr-logs/pull/test-infra/17183/pull-test-infra-bazel/1254406011708510210",
   592  						ID:           "1254406011708510210",
   593  						Started:      time.Unix(1587908709, 0),
   594  						Duration:     436000000000,
   595  						Result:       "SUCCESS",
   596  						commitHash:   "664ba002bc2155e7438b810a1bb7473c55dc1c6a",
   597  					},
   598  				},
   599  			},
   600  			{
   601  				Name: "pull-test-infra-yamllint",
   602  				Link: "/job-history/gs/kubernetes-jenkins/pr-logs/directory/pull-test-infra-yamllint",
   603  				Builds: []buildData{
   604  					{
   605  						index:        0,
   606  						jobName:      "pull-test-infra-yamllint",
   607  						prefix:       "pr-logs/pull/test-infra/17183/pull-test-infra-yamllint/1254406011708510208/",
   608  						SpyglassLink: "/view/gs/kubernetes-jenkins/pr-logs/pull/test-infra/17183/pull-test-infra-yamllint/1254406011708510208",
   609  						ID:           "1254406011708510208",
   610  						Started:      time.Unix(1587908749, 0),
   611  						Duration:     18000000000,
   612  						Result:       "SUCCESS",
   613  						commitHash:   "664ba002bc2155e7438b810a1bb7473c55dc1c6a",
   614  					},
   615  				},
   616  			},
   617  		},
   618  		Commits: []commitData{
   619  			{
   620  				Hash:       "664ba002bc2155e7438b810a1bb7473c55dc1c6a",
   621  				HashPrefix: "664ba00",
   622  				Link:       "https://github.com/kubernetes/test-infra/commit/664ba002bc2155e7438b810a1bb7473c55dc1c6a",
   623  				MaxWidth:   1,
   624  				latest:     time.Unix(1587908749, 0),
   625  			},
   626  		},
   627  	}
   628  
   629  	type args struct {
   630  		url string
   631  	}
   632  	tests := []struct {
   633  		name    string
   634  		args    args
   635  		want    prHistoryTemplate
   636  		wantErr bool
   637  	}{
   638  		{
   639  			name: "get pr history",
   640  			args: args{
   641  				url: "https://prow.k8s.io/pr-history/?org=kubernetes&repo=test-infra&pr=17183",
   642  			},
   643  			want: wantedPRHistory,
   644  		},
   645  	}
   646  	for _, tt := range tests {
   647  		t.Run(tt.name, func(t *testing.T) {
   648  			prHistoryURL, _ := url.Parse(tt.args.url)
   649  			got, err := getPRHistory(context.Background(), prHistoryURL, c, io.NewGCSOpener(fakeGCSClient), nil, nil, "github.com")
   650  			if (err != nil) != tt.wantErr {
   651  				t.Errorf("getPRHistory() error = %v, wantErr %v", err, tt.wantErr)
   652  				return
   653  			}
   654  			if !reflect.DeepEqual(got, tt.want) {
   655  				t.Errorf("getPRHistory() got = %v, want %v", got, tt.want)
   656  			}
   657  		})
   658  	}
   659  }