sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/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 }