github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/deck/job_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  	"errors"
    22  	"net/url"
    23  	"reflect"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/fsouza/fake-gcs-server/fakestorage"
    28  
    29  	"sigs.k8s.io/prow/pkg/config"
    30  	"sigs.k8s.io/prow/pkg/io"
    31  	"sigs.k8s.io/prow/pkg/io/providers"
    32  )
    33  
    34  func TestJobHistURL(t *testing.T) {
    35  	cases := []struct {
    36  		name            string
    37  		address         string
    38  		storageProvider string
    39  		bktName         string
    40  		root            string
    41  		id              uint64
    42  		expErr          bool
    43  	}{
    44  		{
    45  			address:         "http://www.example.com/job-history/foo-bucket/logs/bar-e2e",
    46  			bktName:         "foo-bucket",
    47  			storageProvider: providers.GS,
    48  			root:            "logs/bar-e2e",
    49  			id:              emptyID,
    50  		},
    51  		{
    52  			address:         "http://www.example.com/job-history/foo-bucket/logs/bar-e2e?buildId=",
    53  			bktName:         "foo-bucket",
    54  			storageProvider: providers.GS,
    55  			root:            "logs/bar-e2e",
    56  			id:              emptyID,
    57  		},
    58  		{
    59  			address:         "http://www.example.com/job-history/foo-bucket/logs/bar-e2e?buildId=123456789123456789",
    60  			bktName:         "foo-bucket",
    61  			storageProvider: providers.GS,
    62  			root:            "logs/bar-e2e",
    63  			id:              123456789123456789,
    64  		},
    65  		{
    66  			address:         "http://www.example.com/job-history/gs/foo-bucket/logs/bar-e2e",
    67  			bktName:         "foo-bucket",
    68  			storageProvider: providers.GS,
    69  			root:            "logs/bar-e2e",
    70  			id:              emptyID,
    71  		},
    72  		{
    73  			address:         "http://www.example.com/job-history/gs/foo-bucket/logs/bar-e2e?buildId=",
    74  			bktName:         "foo-bucket",
    75  			storageProvider: providers.GS,
    76  			root:            "logs/bar-e2e",
    77  			id:              emptyID,
    78  		},
    79  		{
    80  			address:         "http://www.example.com/job-history/gs/foo-bucket/logs/bar-e2e?buildId=123456789123456789",
    81  			bktName:         "foo-bucket",
    82  			storageProvider: providers.GS,
    83  			root:            "logs/bar-e2e",
    84  			id:              123456789123456789,
    85  		},
    86  		{
    87  			address:         "http://www.example.com/job-history/s3/foo-bucket/logs/bar-e2e",
    88  			bktName:         "foo-bucket",
    89  			storageProvider: providers.S3,
    90  			root:            "logs/bar-e2e",
    91  			id:              emptyID,
    92  		},
    93  		{
    94  			address:         "http://www.example.com/job-history/s3/foo-bucket/logs/bar-e2e?buildId=",
    95  			bktName:         "foo-bucket",
    96  			storageProvider: providers.S3,
    97  			root:            "logs/bar-e2e",
    98  			id:              emptyID,
    99  		},
   100  		{
   101  			address:         "http://www.example.com/job-history/s3/foo-bucket/logs/bar-e2e?buildId=123456789123456789",
   102  			bktName:         "foo-bucket",
   103  			storageProvider: providers.S3,
   104  			root:            "logs/bar-e2e",
   105  			id:              123456789123456789,
   106  		},
   107  		{
   108  			address: "http://www.example.com/job-history",
   109  			expErr:  true,
   110  		},
   111  		{
   112  			address: "http://www.example.com/job-history/",
   113  			expErr:  true,
   114  		},
   115  		{
   116  			address: "http://www.example.com/job-history/foo-bucket",
   117  			expErr:  true,
   118  		},
   119  		{
   120  			address: "http://www.example.com/job-history/foo-bucket/",
   121  			expErr:  true,
   122  		},
   123  		{
   124  			address: "http://www.example.com/job-history/foo-bucket/logs/bar-e2e?buildId=-738",
   125  			expErr:  true,
   126  		},
   127  		{
   128  			address: "http://www.example.com/job-history/foo-bucket/logs/bar-e2e?buildId=nope",
   129  			expErr:  true,
   130  		},
   131  	}
   132  	for _, tc := range cases {
   133  		u, _ := url.Parse(tc.address)
   134  		storageProvider, bktName, root, id, err := parseJobHistURL(u)
   135  		if tc.expErr {
   136  			if err == nil && tc.expErr {
   137  				t.Errorf("parsing %q: expected error", tc.address)
   138  			}
   139  			continue
   140  		}
   141  		if err != nil {
   142  			t.Errorf("parsing %q: unexpected error: %v", tc.address, err)
   143  		}
   144  		if storageProvider != tc.storageProvider {
   145  			t.Errorf("parsing %q: expected storageProvider %s, got %s", tc.address, tc.storageProvider, storageProvider)
   146  		}
   147  		if bktName != tc.bktName {
   148  			t.Errorf("parsing %q: expected bucket %s, got %s", tc.address, tc.bktName, bktName)
   149  		}
   150  		if root != tc.root {
   151  			t.Errorf("parsing %q: expected root %s, got %s", tc.address, tc.root, root)
   152  		}
   153  		if id != tc.id {
   154  			t.Errorf("parsing %q: expected id %d, got %d", tc.address, tc.id, id)
   155  		}
   156  	}
   157  }
   158  
   159  func eq(a, b []uint64) bool {
   160  	if len(a) != len(b) {
   161  		return false
   162  	}
   163  	for i := 0; i < len(a); i++ {
   164  		if a[i] != b[i] {
   165  			return false
   166  		}
   167  	}
   168  	return true
   169  }
   170  
   171  func TestCropResults(t *testing.T) {
   172  	cases := []struct {
   173  		a   []uint64
   174  		max uint64
   175  		exp []uint64
   176  		p   int
   177  		q   int
   178  	}{
   179  		{
   180  			a:   []uint64{},
   181  			max: 42,
   182  			exp: []uint64{},
   183  			p:   -1,
   184  			q:   0,
   185  		},
   186  		{
   187  			a:   []uint64{81, 27, 9, 3, 1},
   188  			max: 100,
   189  			exp: []uint64{81, 27, 9, 3, 1},
   190  			p:   0,
   191  			q:   4,
   192  		},
   193  		{
   194  			a:   []uint64{81, 27, 9, 3, 1},
   195  			max: 50,
   196  			exp: []uint64{27, 9, 3, 1},
   197  			p:   1,
   198  			q:   4,
   199  		},
   200  		{
   201  			a:   []uint64{25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
   202  			max: 23,
   203  			exp: []uint64{23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4},
   204  			p:   2,
   205  			q:   21,
   206  		},
   207  	}
   208  	for _, tc := range cases {
   209  		actual, firstIndex, lastIndex := cropResults(tc.a, tc.max)
   210  		if !eq(actual, tc.exp) || firstIndex != tc.p || lastIndex != tc.q {
   211  			t.Errorf("cropResults(%v, %d) expected (%v, %d, %d), got (%v, %d, %d)",
   212  				tc.a, tc.max, tc.exp, tc.p, tc.q, actual, firstIndex, lastIndex)
   213  		}
   214  	}
   215  }
   216  
   217  func TestLinkID(t *testing.T) {
   218  	cases := []struct {
   219  		startAddr string
   220  		id        uint64
   221  		expAddr   string
   222  	}{
   223  		{
   224  			startAddr: "http://www.example.com/job-history/foo-bucket/logs/bar-e2e",
   225  			id:        emptyID,
   226  			expAddr:   "http://www.example.com/job-history/foo-bucket/logs/bar-e2e?buildId=",
   227  		},
   228  		{
   229  			startAddr: "http://www.example.com/job-history/foo-bucket/logs/bar-e2e",
   230  			id:        23,
   231  			expAddr:   "http://www.example.com/job-history/foo-bucket/logs/bar-e2e?buildId=23",
   232  		},
   233  	}
   234  	for _, tc := range cases {
   235  		u, _ := url.Parse(tc.startAddr)
   236  		actual := linkID(u, tc.id)
   237  		if actual != tc.expAddr {
   238  			t.Errorf("adding id param %d expected %s, got %s", tc.id, tc.expAddr, actual)
   239  		}
   240  		again, _ := url.Parse(tc.startAddr)
   241  		if again.String() != u.String() {
   242  			t.Errorf("linkID incorrectly mutated URL (expected %s, got %s)", u.String(), again.String())
   243  		}
   244  	}
   245  }
   246  
   247  func Test_getJobHistory(t *testing.T) {
   248  	objects := []fakestorage.Object{
   249  		// pr-logs
   250  		{
   251  			BucketName: "kubernetes-jenkins",
   252  			Name:       "pr-logs/directory/pull-test-infra-bazel/latest-build.txt",
   253  			Content:    []byte("1254406011708510210"),
   254  		},
   255  		{
   256  			BucketName: "kubernetes-jenkins",
   257  			Name:       "pr-logs/directory/pull-test-infra-bazel/1221704015146913792.txt",
   258  			Content:    []byte("gs://kubernetes-jenkins/pr-logs/pull/test-infra/16031/pull-test-infra-bazel/1221704015146913792"),
   259  		},
   260  		{
   261  			BucketName: "kubernetes-jenkins",
   262  			Name:       "pr-logs/pull/test-infra/16031/pull-test-infra-bazel/1221704015146913792/started.json",
   263  			Content:    []byte("{\"timestamp\": 1580111939,\"pull\": \"16031\",\"repo-version\": \"19d9f301988f45d41addec0e307587addedbafdd\",\"repos\": {\"kubernetes/test-infra\": \"master:589aceb353f25b6af6f576f58ba16c71ef8870f3,16031:ec9156a00793375b5ca885b9b1f26be789315c50\"}}"),
   264  		},
   265  		{
   266  			BucketName: "kubernetes-jenkins",
   267  			Name:       "pr-logs/pull/test-infra/16031/pull-test-infra-bazel/1221704015146913792/finished.json",
   268  			Content:    []byte("{\"timestamp\": 1580112259,\"passed\": true,\"result\": \"SUCCESS\",\"revision\": \"ec9156a00793375b5ca885b9b1f26be789315c50\"}"),
   269  		},
   270  		{
   271  			BucketName: "kubernetes-jenkins",
   272  			Name:       "pr-logs/directory/pull-test-infra-bazel/1254406011708510210.txt",
   273  			Content:    []byte("gs://kubernetes-jenkins/pr-logs/pull/test-infra/17183/pull-test-infra-bazel/1254406011708510210"),
   274  		},
   275  		{
   276  			BucketName: "kubernetes-jenkins",
   277  			Name:       "pr-logs/pull/test-infra/17183/pull-test-infra-bazel/1254406011708510210/started.json",
   278  			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}"),
   279  		},
   280  		{
   281  			BucketName: "kubernetes-jenkins",
   282  			Name:       "pr-logs/pull/test-infra/17183/pull-test-infra-bazel/1254406011708510210/finished.json",
   283  			Content:    []byte("{\"timestamp\": 1587909145,\"passed\": true,\"result\": \"SUCCESS\",\"revision\": \"664ba002bc2155e7438b810a1bb7473c55dc1c6a\"}"),
   284  		},
   285  		// logs
   286  		{
   287  			BucketName: "kubernetes-jenkins",
   288  			Name:       "logs/post-cluster-api-provider-openstack-push-images/latest-build.txt",
   289  			Content:    []byte("1253687771944456193"),
   290  		},
   291  		{
   292  			BucketName: "kubernetes-jenkins",
   293  			Name:       "logs/post-cluster-api-provider-openstack-push-images/1253687771944456193/started.json",
   294  			Content:    []byte("{\"timestamp\": 1587737470,\"repos\": {\"kubernetes-sigs/cluster-api-provider-openstack\": \"master:b62656cde943aef3bcd1a18064aecff8b0f30a0c\"},\"metadata\": {\"resultstore\": \"https://source.cloud.google.com/results/invocations/9dce789e-c400-4204-a46c-86a3a5fde6c3/targets/test\"},\"repo-version\": \"b62656cde943aef3bcd1a18064aecff8b0f30a0c\",\"Pending\": false}"),
   295  		},
   296  		{
   297  			BucketName: "kubernetes-jenkins",
   298  			Name:       "logs/post-cluster-api-provider-openstack-push-images/1253687771944456193/finished.json",
   299  			Content:    []byte("{\"timestamp\": 1587738205,\"passed\": true,\"result\": \"SUCCESS\",\"revision\": \"b62656cde943aef3bcd1a18064aecff8b0f30a0c\"}"),
   300  		},
   301  	}
   302  	wantedPRLogsJobHistoryTemplate := jobHistoryTemplate{
   303  		Name:         "pr-logs/directory/pull-test-infra-bazel",
   304  		ResultsShown: 2,
   305  		ResultsTotal: 2,
   306  		Builds: []buildData{
   307  			{
   308  				index:        0,
   309  				SpyglassLink: "/view/gs/kubernetes-jenkins/pr-logs/pull/test-infra/17183/pull-test-infra-bazel/1254406011708510210",
   310  				ID:           "1254406011708510210",
   311  				Started:      time.Unix(1587908709, 0),
   312  				Duration:     436000000000,
   313  				Result:       "SUCCESS",
   314  				commitHash:   "664ba002bc2155e7438b810a1bb7473c55dc1c6a",
   315  			},
   316  			{
   317  				index:        1,
   318  				SpyglassLink: "/view/gs/kubernetes-jenkins/pr-logs/pull/test-infra/16031/pull-test-infra-bazel/1221704015146913792",
   319  				ID:           "1221704015146913792",
   320  				Started:      time.Unix(1580111939, 0),
   321  				Duration:     320000000000,
   322  				Result:       "SUCCESS",
   323  				commitHash:   "ec9156a00793375b5ca885b9b1f26be789315c50",
   324  			},
   325  		},
   326  	}
   327  	wantedLogsJobHistoryTemplate := jobHistoryTemplate{
   328  		Name:         "logs/post-cluster-api-provider-openstack-push-images",
   329  		ResultsShown: 1,
   330  		ResultsTotal: 1,
   331  		Builds: []buildData{
   332  			{
   333  				index:        0,
   334  				SpyglassLink: "/view/gs/kubernetes-jenkins/logs/post-cluster-api-provider-openstack-push-images/1253687771944456193",
   335  				ID:           "1253687771944456193",
   336  				Started:      time.Unix(1587737470, 0),
   337  				Duration:     735000000000,
   338  				Result:       "SUCCESS",
   339  				commitHash:   "b62656cde943aef3bcd1a18064aecff8b0f30a0c",
   340  			},
   341  		},
   342  	}
   343  	gcsServer := fakestorage.NewServer(objects)
   344  	defer gcsServer.Stop()
   345  
   346  	fakeGCSClient := gcsServer.Client()
   347  
   348  	boolTrue := true
   349  	ca := &config.Agent{}
   350  	ca.Set(&config.Config{
   351  		ProwConfig: config.ProwConfig{
   352  			Deck: config.Deck{
   353  				SkipStoragePathValidation: &boolTrue,
   354  				Spyglass: config.Spyglass{
   355  					BucketAliases: map[string]string{"kubernetes-jenkins-old": "kubernetes-jenkins"},
   356  				},
   357  			},
   358  		},
   359  	})
   360  
   361  	tests := []struct {
   362  		name    string
   363  		url     string
   364  		want    jobHistoryTemplate
   365  		wantErr string
   366  	}{
   367  		{
   368  			name: "get job history pr-logs (old format)",
   369  			url:  "https://prow.k8s.io/job-history/kubernetes-jenkins/pr-logs/directory/pull-test-infra-bazel",
   370  			want: wantedPRLogsJobHistoryTemplate,
   371  		},
   372  		{
   373  			name: "get job history pr-logs (new format)",
   374  			url:  "https://prow.k8s.io/job-history/gs/kubernetes-jenkins/pr-logs/directory/pull-test-infra-bazel",
   375  			want: wantedPRLogsJobHistoryTemplate,
   376  		},
   377  		{
   378  			name: "get job history logs (old format)",
   379  			url:  "https://prow.k8s.io/job-history/kubernetes-jenkins/logs/post-cluster-api-provider-openstack-push-images",
   380  			want: wantedLogsJobHistoryTemplate,
   381  		},
   382  		{
   383  			name: "get job history logs (new format)",
   384  			url:  "https://prow.k8s.io/job-history/gs/kubernetes-jenkins/logs/post-cluster-api-provider-openstack-push-images",
   385  			want: wantedLogsJobHistoryTemplate,
   386  		},
   387  		{
   388  			name: "get job history logs through a bucket alias (new format)",
   389  			url:  "https://prow.k8s.io/job-history/gs/kubernetes-jenkins-old/logs/post-cluster-api-provider-openstack-push-images",
   390  			want: wantedLogsJobHistoryTemplate,
   391  		},
   392  	}
   393  
   394  	for _, tt := range tests {
   395  		t.Run(tt.name, func(t *testing.T) {
   396  			jobURL, _ := url.Parse(tt.url)
   397  			got, err := getJobHistory(context.Background(), jobURL, ca.Config, io.NewGCSOpener(fakeGCSClient))
   398  			var actualErr string
   399  			if err != nil {
   400  				actualErr = err.Error()
   401  			}
   402  			if actualErr != tt.wantErr {
   403  				t.Errorf("getJobHistory() error = %v, wantErr %v", actualErr, tt.wantErr)
   404  				return
   405  			}
   406  			if !reflect.DeepEqual(got, tt.want) {
   407  				t.Errorf("getJobHistory() got = %v, want %v", got, tt.want)
   408  			}
   409  		})
   410  	}
   411  }
   412  
   413  // TestListBuildIDsReturnsResultsOnError verifies that we get results even when there was an error,
   414  // mostly important so we can timeout it and still get some results.
   415  func TestListBuildIDsReturnsResultsOnError(t *testing.T) {
   416  	t.Run("logs-prefix", func(t *testing.T) {
   417  		bucket := blobStorageBucket{Opener: fakeOpener{iterator: fakeIterator{
   418  			result: io.ObjectAttributes{Name: "13728953029057617923", IsDir: true},
   419  			err:    errors.New("some-err"),
   420  		}}}
   421  		ids, err := bucket.listBuildIDs(context.Background(), logsPrefix)
   422  		if err == nil || err.Error() != "failed to list directories: some-err" {
   423  			t.Fatalf("didn't get expected error message 'failed to list directories: some-err' but got err %v", err)
   424  		}
   425  		if n := len(ids); n != 1 {
   426  			t.Errorf("didn't get result back, ids were %v", ids)
   427  		}
   428  	})
   429  	t.Run("no-prefix", func(t *testing.T) {
   430  		bucket := blobStorageBucket{Opener: fakeOpener{iterator: fakeIterator{
   431  			result: io.ObjectAttributes{Name: "/13728953029057617923.txt", IsDir: false},
   432  			err:    errors.New("some-err"),
   433  		}}}
   434  		ids, err := bucket.listBuildIDs(context.Background(), "")
   435  		if err == nil || err.Error() != "failed to list keys: some-err" {
   436  			t.Fatalf("didn't get expected error message 'failed to list keys: some-err' but got err %v", err)
   437  		}
   438  		if n := len(ids); n != 1 {
   439  			t.Errorf("didn't get result back, ids were %v", ids)
   440  		}
   441  	})
   442  }
   443  
   444  type fakeIterator struct {
   445  	ranOnce bool
   446  	result  io.ObjectAttributes
   447  	err     error
   448  }
   449  
   450  func (fi *fakeIterator) Next(_ context.Context) (io.ObjectAttributes, error) {
   451  	if !fi.ranOnce {
   452  		fi.ranOnce = true
   453  		return fi.result, nil
   454  	}
   455  	return io.ObjectAttributes{}, fi.err
   456  }
   457  
   458  type fakeOpener struct {
   459  	io.Opener
   460  	iterator fakeIterator
   461  }
   462  
   463  func (fo fakeOpener) Iterator(_ context.Context, _, _ string) (io.ObjectIterator, error) {
   464  	return &fo.iterator, nil
   465  }