go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/batch_create_artifacts_test.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package recorder 16 17 import ( 18 "context" 19 "crypto/sha256" 20 "encoding/hex" 21 "fmt" 22 "testing" 23 "time" 24 25 repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 26 spb "google.golang.org/genproto/googleapis/rpc/status" 27 "google.golang.org/grpc" 28 "google.golang.org/grpc/codes" 29 "google.golang.org/grpc/metadata" 30 "google.golang.org/protobuf/types/known/timestamppb" 31 32 "go.chromium.org/luci/common/errors" 33 "go.chromium.org/luci/common/tsmon" 34 "go.chromium.org/luci/server/auth" 35 "go.chromium.org/luci/server/auth/authtest" 36 37 "go.chromium.org/luci/resultdb/bqutil" 38 "go.chromium.org/luci/resultdb/internal/artifacts" 39 "go.chromium.org/luci/resultdb/internal/config" 40 "go.chromium.org/luci/resultdb/internal/invocations" 41 "go.chromium.org/luci/resultdb/internal/spanutil" 42 "go.chromium.org/luci/resultdb/internal/testutil" 43 "go.chromium.org/luci/resultdb/internal/testutil/insert" 44 bqpb "go.chromium.org/luci/resultdb/proto/bq" 45 configpb "go.chromium.org/luci/resultdb/proto/config" 46 pb "go.chromium.org/luci/resultdb/proto/v1" 47 48 . "github.com/smartystreets/goconvey/convey" 49 . "go.chromium.org/luci/common/testing/assertions" 50 ) 51 52 // fakeRBEClient mocks BatchUpdateBlobs. 53 type fakeRBEClient struct { 54 repb.ContentAddressableStorageClient 55 req *repb.BatchUpdateBlobsRequest 56 resp *repb.BatchUpdateBlobsResponse 57 err error 58 } 59 60 func (c *fakeRBEClient) BatchUpdateBlobs(ctx context.Context, in *repb.BatchUpdateBlobsRequest, opts ...grpc.CallOption) (*repb.BatchUpdateBlobsResponse, error) { 61 c.req = in 62 return c.resp, c.err 63 } 64 65 func (c *fakeRBEClient) mockResp(err error, cds ...codes.Code) { 66 c.err = err 67 c.resp = &repb.BatchUpdateBlobsResponse{} 68 for _, cd := range cds { 69 c.resp.Responses = append(c.resp.Responses, &repb.BatchUpdateBlobsResponse_Response{ 70 Status: &spb.Status{Code: int32(cd)}, 71 }) 72 } 73 } 74 75 type fakeBQClient struct { 76 Rows []*bqpb.TextArtifactRow 77 Error error 78 } 79 80 func (c *fakeBQClient) InsertArtifactRows(ctx context.Context, rows []*bqpb.TextArtifactRow) error { 81 if c.Error != nil { 82 return c.Error 83 } 84 c.Rows = append(c.Rows, rows...) 85 return nil 86 } 87 88 func TestNewArtifactCreationRequestsFromProto(t *testing.T) { 89 newArtReq := func(parent, artID, contentType string) *pb.CreateArtifactRequest { 90 return &pb.CreateArtifactRequest{ 91 Parent: parent, 92 Artifact: &pb.Artifact{ArtifactId: artID, ContentType: contentType}, 93 } 94 } 95 96 Convey("newArtifactCreationRequestsFromProto", t, func() { 97 bReq := &pb.BatchCreateArtifactsRequest{} 98 invArt := newArtReq("invocations/inv1", "art1", "text/html") 99 trArt := newArtReq("invocations/inv1/tests/t1/results/r1", "art2", "image/png") 100 101 Convey("successes", func() { 102 bReq.Requests = append(bReq.Requests, invArt) 103 bReq.Requests = append(bReq.Requests, trArt) 104 invID, arts, err := parseBatchCreateArtifactsRequest(bReq) 105 So(err, ShouldBeNil) 106 So(invID, ShouldEqual, invocations.ID("inv1")) 107 So(len(arts), ShouldEqual, len(bReq.Requests)) 108 109 // invocation-level artifact 110 So(arts[0].artifactID, ShouldEqual, "art1") 111 So(arts[0].parentID(), ShouldEqual, artifacts.ParentID("", "")) 112 So(arts[0].contentType, ShouldEqual, "text/html") 113 114 // test-result-level artifact 115 So(arts[1].artifactID, ShouldEqual, "art2") 116 So(arts[1].parentID(), ShouldEqual, artifacts.ParentID("t1", "r1")) 117 So(arts[1].contentType, ShouldEqual, "image/png") 118 }) 119 120 Convey("mismatched size_bytes", func() { 121 bReq.Requests = append(bReq.Requests, trArt) 122 trArt.Artifact.SizeBytes = 123 123 trArt.Artifact.Contents = make([]byte, 10249) 124 _, _, err := parseBatchCreateArtifactsRequest(bReq) 125 So(err, ShouldErrLike, `sizeBytes and contents are specified but don't match`) 126 }) 127 128 Convey("ignored size_bytes", func() { 129 bReq.Requests = append(bReq.Requests, trArt) 130 trArt.Artifact.SizeBytes = 0 131 trArt.Artifact.Contents = make([]byte, 10249) 132 _, arts, err := parseBatchCreateArtifactsRequest(bReq) 133 So(err, ShouldBeNil) 134 So(arts[0].size, ShouldEqual, 10249) 135 }) 136 137 Convey("contents and gcs_uri both specified", func() { 138 bReq.Requests = append(bReq.Requests, trArt) 139 trArt.Artifact.SizeBytes = 0 140 trArt.Artifact.Contents = make([]byte, 10249) 141 trArt.Artifact.GcsUri = "gs://testbucket/testfile" 142 _, _, err := parseBatchCreateArtifactsRequest(bReq) 143 So(err, ShouldErrLike, `only one of contents and gcs_uri can be given`) 144 }) 145 146 Convey("sum() of artifact.Contents is too big", func() { 147 for i := 0; i < 11; i++ { 148 req := newArtReq("invocations/inv1", fmt.Sprintf("art%d", i), "text/html") 149 req.Artifact.Contents = make([]byte, 1024*1024) 150 bReq.Requests = append(bReq.Requests, req) 151 } 152 _, _, err := parseBatchCreateArtifactsRequest(bReq) 153 So(err, ShouldErrLike, "the total size of artifact contents exceeded") 154 }) 155 156 Convey("if more than one invocations", func() { 157 bReq.Requests = append(bReq.Requests, newArtReq("invocations/inv1", "art1", "text/html")) 158 bReq.Requests = append(bReq.Requests, newArtReq("invocations/inv2", "art1", "text/html")) 159 _, _, err := parseBatchCreateArtifactsRequest(bReq) 160 So(err, ShouldErrLike, `only one invocation is allowed: "inv1", "inv2"`) 161 }) 162 }) 163 } 164 165 func TestBatchCreateArtifacts(t *testing.T) { 166 // metric field values for Artifact table 167 artMFVs := []any{string(spanutil.Artifacts), string(spanutil.Inserted), insert.TestRealm} 168 169 Convey("TestBatchCreateArtifacts", t, func() { 170 ctx := testutil.SpannerTestContext(t) 171 ctx = testutil.TestProjectConfigContext(ctx, "testproject", "user:test@test.com", "testbucket") 172 token, err := generateInvocationToken(ctx, "inv") 173 So(err, ShouldBeNil) 174 ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(pb.UpdateTokenMetadataKey, token)) 175 ctx, _ = tsmon.WithDummyInMemory(ctx) 176 store := tsmon.Store(ctx) 177 ctx = auth.WithState(ctx, &authtest.FakeState{ 178 Identity: "user:test@test.com", 179 }) 180 181 err = config.SetServiceConfig(ctx, &configpb.Config{ 182 BqArtifactExportConfig: &configpb.BqArtifactExportConfig{ 183 Enabled: true, 184 ExportPercent: 100, 185 }, 186 }) 187 So(err, ShouldBeNil) 188 189 casClient := &fakeRBEClient{} 190 recorder := newTestRecorderServer() 191 recorder.casClient = casClient 192 bReq := &pb.BatchCreateArtifactsRequest{} 193 194 appendArtReq := func(aID, content, cType, parent string) { 195 bReq.Requests = append(bReq.Requests, &pb.CreateArtifactRequest{ 196 Parent: parent, 197 Artifact: &pb.Artifact{ 198 ArtifactId: aID, Contents: []byte(content), ContentType: cType, 199 }, 200 }) 201 } 202 appendGcsArtReq := func(aID string, cSize int64, cType string, gcsURI string) { 203 bReq.Requests = append(bReq.Requests, &pb.CreateArtifactRequest{ 204 Parent: "invocations/inv", 205 Artifact: &pb.Artifact{ 206 ArtifactId: aID, SizeBytes: cSize, ContentType: cType, GcsUri: gcsURI, 207 }, 208 }) 209 } 210 211 fetchState := func(parentID, aID string) (size int64, hash string, contentType string, gcsURI string) { 212 testutil.MustReadRow( 213 ctx, "Artifacts", invocations.ID("inv").Key(parentID, aID), 214 map[string]any{ 215 "Size": &size, 216 "RBECASHash": &hash, 217 "ContentType": &contentType, 218 "GcsURI": &gcsURI, 219 }, 220 ) 221 return 222 } 223 compHash := func(content string) string { 224 h := sha256.Sum256([]byte(content)) 225 return hex.EncodeToString(h[:]) 226 } 227 228 Convey("GCS reference isAllowed", func() { 229 Convey("reference is allowed", func() { 230 testutil.SetGCSAllowedBuckets(ctx, "testproject", "user:test@test.com", "testbucket") 231 232 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 233 appendGcsArtReq("art1", 0, "text/plain", "gs://testbucket/art1") 234 235 _, err := recorder.BatchCreateArtifacts(ctx, bReq) 236 So(err, ShouldBeNil) 237 }) 238 Convey("project not configured", func() { 239 testutil.SetGCSAllowedBuckets(ctx, "otherproject", "user:test@test.com", "testbucket") 240 241 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 242 appendGcsArtReq("art1", 0, "text/plain", "gs://testbucket/art1") 243 244 _, err := recorder.BatchCreateArtifacts(ctx, bReq) 245 So(err, ShouldNotBeNil) 246 So(err.Error(), ShouldContainSubstring, "testproject") 247 }) 248 Convey("user not configured", func() { 249 testutil.SetGCSAllowedBuckets(ctx, "testproject", "user:test@test.com", "testbucket") 250 ctx = auth.WithState(ctx, &authtest.FakeState{ 251 Identity: "user:other@test.com", 252 }) 253 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 254 appendGcsArtReq("art1", 0, "text/plain", "gs://testbucket/art1") 255 256 _, err := recorder.BatchCreateArtifacts(ctx, bReq) 257 So(err, ShouldNotBeNil) 258 So(err.Error(), ShouldContainSubstring, "user:other@test.com") 259 }) 260 Convey("bucket not listed", func() { 261 testutil.SetGCSAllowedBuckets(ctx, "testproject", "user:test@test.com", "otherbucket") 262 263 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 264 appendGcsArtReq("art1", 0, "text/plain", "gs://testbucket/art1") 265 266 _, err := recorder.BatchCreateArtifacts(ctx, bReq) 267 So(err, ShouldNotBeNil) 268 So(err.Error(), ShouldContainSubstring, "testbucket") 269 }) 270 }) 271 Convey("works", func() { 272 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, map[string]any{ 273 "CreateTime": time.Unix(10000, 0), 274 })) 275 appendArtReq("art1", "c0ntent", "text/plain", "invocations/inv") 276 appendArtReq("art2", "c1ntent", "text/richtext", "invocations/inv/tests/test_id/results/result_id") 277 appendGcsArtReq("art3", 0, "text/plain", "gs://testbucket/art3") 278 appendGcsArtReq("art4", 500, "text/richtext", "gs://testbucket/art4") 279 280 casClient.mockResp(nil, codes.OK, codes.OK) 281 bqClient := &fakeBQClient{} 282 recorder.bqExportClient = bqClient 283 284 resp, err := recorder.BatchCreateArtifacts(ctx, bReq) 285 So(err, ShouldBeNil) 286 So(resp, ShouldResemble, &pb.BatchCreateArtifactsResponse{ 287 Artifacts: []*pb.Artifact{ 288 { 289 Name: "invocations/inv/artifacts/art1", 290 ArtifactId: "art1", 291 ContentType: "text/plain", 292 SizeBytes: 7, 293 }, 294 { 295 Name: "invocations/inv/tests/test_id/results/result_id/artifacts/art2", 296 ArtifactId: "art2", 297 ContentType: "text/richtext", 298 SizeBytes: 7, 299 }, 300 { 301 Name: "invocations/inv/artifacts/art3", 302 ArtifactId: "art3", 303 ContentType: "text/plain", 304 SizeBytes: 0, 305 }, 306 { 307 Name: "invocations/inv/artifacts/art4", 308 ArtifactId: "art4", 309 ContentType: "text/richtext", 310 SizeBytes: 500, 311 }, 312 }, 313 }) 314 // verify the RBECAS reqs 315 So(casClient.req, ShouldResemble, &repb.BatchUpdateBlobsRequest{ 316 InstanceName: "", 317 Requests: []*repb.BatchUpdateBlobsRequest_Request{ 318 { 319 Digest: &repb.Digest{ 320 Hash: compHash("c0ntent"), 321 SizeBytes: int64(len("c0ntent")), 322 }, 323 Data: []byte("c0ntent"), 324 }, 325 { 326 Digest: &repb.Digest{ 327 Hash: compHash("c1ntent"), 328 SizeBytes: int64(len("c1ntent")), 329 }, 330 Data: []byte("c1ntent"), 331 }, 332 }, 333 }) 334 // verify the Spanner states 335 size, hash, cType, gcsURI := fetchState("", "art1") 336 So(size, ShouldEqual, int64(len("c0ntent"))) 337 So(hash, ShouldEqual, artifacts.AddHashPrefix(compHash("c0ntent"))) 338 So(cType, ShouldEqual, "text/plain") 339 So(gcsURI, ShouldEqual, "") 340 341 size, hash, cType, gcsURI = fetchState("tr/test_id/result_id", "art2") 342 So(size, ShouldEqual, int64(len("c1ntent"))) 343 So(hash, ShouldEqual, artifacts.AddHashPrefix(compHash("c1ntent"))) 344 So(cType, ShouldEqual, "text/richtext") 345 So(gcsURI, ShouldEqual, "") 346 347 size, hash, cType, gcsURI = fetchState("", "art3") 348 So(size, ShouldEqual, 0) 349 So(hash, ShouldEqual, "") 350 So(cType, ShouldEqual, "text/plain") 351 So(gcsURI, ShouldEqual, "gs://testbucket/art3") 352 353 size, hash, cType, gcsURI = fetchState("", "art4") 354 So(size, ShouldEqual, 500) 355 So(hash, ShouldEqual, "") 356 So(cType, ShouldEqual, "text/richtext") 357 So(gcsURI, ShouldEqual, "gs://testbucket/art4") 358 359 // RowCount metric should be increased by 4. 360 So(store.Get(ctx, spanutil.RowCounter, time.Time{}, artMFVs), ShouldEqual, 4) 361 362 // Verify the bigquery rows. 363 So(len(bqClient.Rows), ShouldEqual, 2) 364 So(bqClient.Rows, ShouldResembleProto, []*bqpb.TextArtifactRow{ 365 { 366 Project: "testproject", 367 Realm: "testrealm", 368 InvocationId: "inv", 369 ArtifactId: "art1", 370 ContentType: "text/plain", 371 Content: "c0ntent", 372 NumShards: 1, 373 ShardId: 0, 374 ShardContentSize: 7, 375 ArtifactContentSize: 7, 376 PartitionTime: timestamppb.New(time.Unix(10000, 0)), 377 ArtifactShard: "art1:0", 378 }, 379 { 380 Project: "testproject", 381 Realm: "testrealm", 382 InvocationId: "inv", 383 ArtifactId: "art2", 384 ContentType: "text/richtext", 385 Content: "c1ntent", 386 NumShards: 1, 387 ShardId: 0, 388 ShardContentSize: 7, 389 ArtifactContentSize: 7, 390 PartitionTime: timestamppb.New(time.Unix(10000, 0)), 391 TestId: "test_id", 392 ResultId: "result_id", 393 ArtifactShard: "art2:0", 394 }, 395 }) 396 So(artifactExportCounter.Get(ctx, "testproject", "success"), ShouldEqual, 2) 397 }) 398 399 Convey("BatchUpdateBlobs fails", func() { 400 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 401 appendArtReq("art1", "c0ntent", "text/plain", "invocations/inv") 402 appendArtReq("art2", "c1ntent", "text/richtext", "invocations/inv") 403 404 Convey("Partly", func() { 405 casClient.mockResp(nil, codes.OK, codes.InvalidArgument) 406 _, err := recorder.BatchCreateArtifacts(ctx, bReq) 407 So(err, ShouldErrLike, `artifact "invocations/inv/artifacts/art2": cas.BatchUpdateBlobs failed`) 408 }) 409 410 Convey("Entirely", func() { 411 // exceeded the maximum size limit is the only possible error that 412 // can cause the entire request failed. 413 casClient.mockResp(errors.New("err"), codes.OK, codes.OK) 414 _, err := recorder.BatchCreateArtifacts(ctx, bReq) 415 So(err, ShouldErrLike, "cas.BatchUpdateBlobs failed") 416 }) 417 418 // RowCount metric should have no changes from any of the above Convey()s. 419 So(store.Get(ctx, spanutil.RowCounter, time.Time{}, artMFVs), ShouldBeNil) 420 }) 421 422 Convey("Token", func() { 423 appendArtReq("art1", "", "text/plain", "invocations/inv") 424 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 425 426 Convey("Missing", func() { 427 ctx = metadata.NewIncomingContext(ctx, metadata.Pairs()) 428 _, err = recorder.BatchCreateArtifacts(ctx, bReq) 429 So(err, ShouldHaveAppStatus, codes.Unauthenticated, `missing update-token`) 430 }) 431 Convey("Wrong", func() { 432 ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(pb.UpdateTokenMetadataKey, "rong")) 433 _, err = recorder.BatchCreateArtifacts(ctx, bReq) 434 So(err, ShouldHaveAppStatus, codes.PermissionDenied, `invalid update token`) 435 }) 436 }) 437 438 Convey("Verify state", func() { 439 casClient.mockResp(nil, codes.OK, codes.OK) 440 441 Convey("Finalized invocation", func() { 442 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_FINALIZED, nil)) 443 appendArtReq("art1", "c0ntent", "text/plain", "invocations/inv") 444 _, err = recorder.BatchCreateArtifacts(ctx, bReq) 445 So(err, ShouldHaveAppStatus, codes.FailedPrecondition, `invocations/inv is not active`) 446 }) 447 448 art := map[string]any{ 449 "InvocationId": invocations.ID("inv"), 450 "ParentId": "", 451 "ArtifactId": "art1", 452 "RBECASHash": artifacts.AddHashPrefix(compHash("c0ntent")), 453 "Size": len("c0ntent"), 454 "ContentType": "text/plain", 455 } 456 457 gcsArt := map[string]any{ 458 "InvocationId": invocations.ID("inv"), 459 "ParentId": "", 460 "ArtifactId": "art1", 461 "ContentType": "text/plain", 462 "GcsURI": "gs://testbucket/art1", 463 } 464 465 Convey("Same artifact exists", func() { 466 testutil.MustApply(ctx, 467 insert.Invocation("inv", pb.Invocation_ACTIVE, nil), 468 spanutil.InsertMap("Artifacts", art), 469 ) 470 appendArtReq("art1", "c0ntent", "text/plain", "invocations/inv") 471 resp, err := recorder.BatchCreateArtifacts(ctx, bReq) 472 So(err, ShouldBeNil) 473 So(resp, ShouldResemble, &pb.BatchCreateArtifactsResponse{}) 474 }) 475 476 Convey("Different artifact exists", func() { 477 testutil.SetGCSAllowedBuckets(ctx, "testproject", "user:test@test.com", "testbucket") 478 testutil.MustApply(ctx, 479 insert.Invocation("inv", pb.Invocation_ACTIVE, nil), 480 spanutil.InsertMap("Artifacts", art), 481 ) 482 483 appendArtReq("art1", "c0ntent", "text/plain", "invocations/inv") 484 bReq.Requests[0].Artifact.Contents = []byte("loooong content") 485 _, err := recorder.BatchCreateArtifacts(ctx, bReq) 486 So(err, ShouldHaveAppStatus, codes.AlreadyExists, "exists w/ different size") 487 488 bReq.Requests[0].Artifact.Contents = []byte("c1ntent") 489 _, err = recorder.BatchCreateArtifacts(ctx, bReq) 490 So(err, ShouldHaveAppStatus, codes.AlreadyExists, "exists w/ different hash") 491 492 bReq.Requests[0].Artifact.Contents = []byte("") 493 bReq.Requests[0].Artifact.GcsUri = "gs://testbucket/art1" 494 _, err = recorder.BatchCreateArtifacts(ctx, bReq) 495 So(err, ShouldHaveAppStatus, codes.AlreadyExists, "exists w/ different storage scheme") 496 }) 497 498 Convey("Different artifact exists GCS", func() { 499 testutil.SetGCSAllowedBuckets(ctx, "testproject", "user:test@test.com", "testbucket") 500 testutil.MustApply(ctx, 501 insert.Invocation("inv", pb.Invocation_ACTIVE, nil), 502 spanutil.InsertMap("Artifacts", gcsArt), 503 ) 504 505 appendArtReq("art1", "c0ntent", "text/plain", "invocations/inv") 506 _, err := recorder.BatchCreateArtifacts(ctx, bReq) 507 So(err, ShouldHaveAppStatus, codes.AlreadyExists, "exists w/ different storage scheme") 508 509 bReq.Requests[0].Artifact.Contents = []byte("") 510 bReq.Requests[0].Artifact.GcsUri = "gs://testbucket/art2" 511 _, err = recorder.BatchCreateArtifacts(ctx, bReq) 512 So(err, ShouldHaveAppStatus, codes.AlreadyExists, "exists w/ different GCS URI") 513 514 appendArtReq("art1", "c0ntent", "text/plain", "invocations/inv") 515 bReq.Requests[0].Artifact.SizeBytes = 42 516 _, err = recorder.BatchCreateArtifacts(ctx, bReq) 517 So(err, ShouldHaveAppStatus, codes.AlreadyExists, "exists w/ different size") 518 }) 519 520 // RowCount metric should have no changes from any of the above Convey()s. 521 So(store.Get(ctx, spanutil.RowCounter, time.Time{}, artMFVs), ShouldBeNil) 522 }) 523 524 Convey("Too many requests", func() { 525 bReq.Requests = make([]*pb.CreateArtifactRequest, 1000) 526 _, err := recorder.BatchCreateArtifacts(ctx, bReq) 527 So(err, ShouldHaveAppStatus, codes.InvalidArgument, "the number of requests in the batch exceeds 500") 528 }) 529 }) 530 } 531 532 func TestFilterAndThrottle(t *testing.T) { 533 ctx := context.Background() 534 // Setup tsmon 535 ctx, _ = tsmon.WithDummyInMemory(ctx) 536 537 invInfo := &invocationInfo{realm: "chromium:ci"} 538 Convey("Filter artifact", t, func() { 539 artReqs := []*artifactCreationRequest{ 540 { 541 testID: "test1", 542 contentType: "", 543 }, 544 { 545 testID: "test2", 546 contentType: "text/plain", 547 }, 548 { 549 testID: "test3", 550 contentType: "image/png", 551 }, 552 { 553 testID: "test4", 554 contentType: "text/html", 555 }, 556 } 557 results := filterTextArtifactRequests(ctx, artReqs, invInfo) 558 So(results, ShouldResemble, []*artifactCreationRequest{ 559 { 560 testID: "test2", 561 contentType: "text/plain", 562 }, 563 { 564 testID: "test4", 565 contentType: "text/html", 566 }, 567 }) 568 So(artifactContentCounter.Get(ctx, "chromium", "text"), ShouldEqual, 2) 569 So(artifactContentCounter.Get(ctx, "chromium", "nontext"), ShouldEqual, 1) 570 So(artifactContentCounter.Get(ctx, "chromium", "empty"), ShouldEqual, 1) 571 }) 572 573 Convey("Throttle artifact", t, func() { 574 artReqs := []*artifactCreationRequest{ 575 { 576 testID: "test", 577 artifactID: "artifact38", // Hash value 0 578 }, 579 { 580 testID: "test", 581 artifactID: "artifact158", // Hash value 99 582 }, 583 { 584 testID: "test", 585 artifactID: "artifact230", // Hash value 32 586 }, 587 { 588 testID: "test", 589 artifactID: "artifact232", // Hash value 54 590 }, 591 { 592 testID: "test", 593 artifactID: "artifact341", // Hash value 91 594 }, 595 } 596 // 0%. 597 results, err := throttleArtifactsForBQ(artReqs, 0) 598 So(err, ShouldBeNil) 599 So(results, ShouldResemble, []*artifactCreationRequest{}) 600 601 // 1%. 602 results, err = throttleArtifactsForBQ(artReqs, 1) 603 So(err, ShouldBeNil) 604 So(results, ShouldResemble, []*artifactCreationRequest{ 605 { 606 testID: "test", 607 artifactID: "artifact38", // Hash value 0 608 }, 609 }) 610 611 // 33%. 612 results, err = throttleArtifactsForBQ(artReqs, 33) 613 So(err, ShouldBeNil) 614 So(results, ShouldResemble, []*artifactCreationRequest{ 615 { 616 testID: "test", 617 artifactID: "artifact38", // Hash value 0 618 }, 619 { 620 testID: "test", 621 artifactID: "artifact230", // Hash value 32 622 }, 623 }) 624 625 // 90%. 626 results, err = throttleArtifactsForBQ(artReqs, 90) 627 So(err, ShouldBeNil) 628 So(results, ShouldResemble, []*artifactCreationRequest{ 629 { 630 testID: "test", 631 artifactID: "artifact38", // Hash value 0 632 }, 633 { 634 testID: "test", 635 artifactID: "artifact230", // Hash value 32 636 }, 637 { 638 testID: "test", 639 artifactID: "artifact232", // Hash value 54 640 }, 641 }) 642 643 // 100%. 644 results, err = throttleArtifactsForBQ(artReqs, 100) 645 So(err, ShouldBeNil) 646 So(results, ShouldResemble, []*artifactCreationRequest{ 647 { 648 testID: "test", 649 artifactID: "artifact38", // Hash value 0 650 }, 651 { 652 testID: "test", 653 artifactID: "artifact158", // Hash value 99 654 }, 655 { 656 testID: "test", 657 artifactID: "artifact230", // Hash value 32 658 }, 659 { 660 testID: "test", 661 artifactID: "artifact232", // Hash value 54 662 }, 663 { 664 testID: "test", 665 artifactID: "artifact341", // Hash value 91 666 }, 667 }) 668 }) 669 } 670 671 func TestReqToProto(t *testing.T) { 672 Convey("Artifact request to proto", t, func() { 673 ctx := context.Background() 674 req := &artifactCreationRequest{ 675 testID: "testid", 676 resultID: "resultid", 677 artifactID: "artifactid", 678 contentType: "text/plain", 679 size: 20, 680 data: []byte("0123456789abcdefghij"), 681 } 682 invInfo := &invocationInfo{ 683 id: "invid", 684 realm: "chromium:ci", 685 createTime: time.Unix(1000, 0), 686 } 687 results, err := reqToProtos(ctx, req, invInfo, 10, 10) 688 So(err, ShouldBeNil) 689 So(results, ShouldResembleProto, []*bqpb.TextArtifactRow{ 690 { 691 Project: "chromium", 692 Realm: "ci", 693 InvocationId: "invid", 694 TestId: "testid", 695 ResultId: "resultid", 696 ArtifactId: "artifactid", 697 ContentType: "text/plain", 698 NumShards: 2, 699 ShardId: 0, 700 Content: "0123456789", 701 ShardContentSize: 10, 702 ArtifactContentSize: 20, 703 PartitionTime: timestamppb.New(time.Unix(1000, 0)), 704 ArtifactShard: "artifactid:0", 705 }, 706 { 707 Project: "chromium", 708 Realm: "ci", 709 InvocationId: "invid", 710 TestId: "testid", 711 ResultId: "resultid", 712 ArtifactId: "artifactid", 713 ContentType: "text/plain", 714 NumShards: 2, 715 ShardId: 1, 716 Content: "abcdefghij", 717 ShardContentSize: 10, 718 ArtifactContentSize: 20, 719 PartitionTime: timestamppb.New(time.Unix(1000, 0)), 720 ArtifactShard: "artifactid:1", 721 }, 722 }) 723 }) 724 } 725 726 func TestArtifactExportCounter(t *testing.T) { 727 Convey("BQ export error", t, func() { 728 ctx := context.Background() 729 ctx, _ = tsmon.WithDummyInMemory(ctx) 730 client := &fakeBQClient{ 731 Error: fmt.Errorf("Some error"), 732 } 733 invInfo := &invocationInfo{ 734 id: "invid", 735 realm: "chromium:ci", 736 createTime: time.Unix(1000, 0), 737 } 738 reqs := []*artifactCreationRequest{ 739 { 740 testID: "testid", 741 resultID: "resultid", 742 artifactID: "artifactid", 743 contentType: "text/plain", 744 size: 20, 745 data: []byte("0123456789abcdefghij"), 746 }, 747 } 748 err := uploadArtifactsToBQ(ctx, client, reqs, invInfo) 749 So(err, ShouldNotBeNil) 750 So(artifactExportCounter.Get(ctx, "chromium", "failure_bq"), ShouldEqual, 1) 751 }) 752 753 Convey("Input error", t, func() { 754 ctx := context.Background() 755 ctx, _ = tsmon.WithDummyInMemory(ctx) 756 client := &fakeBQClient{ 757 Error: errors.Annotate(fmt.Errorf("error"), "marshal proto").Tag(errors.BoolTag{Key: bqutil.InvalidRowTagKey}).Err(), 758 } 759 invInfo := &invocationInfo{ 760 id: "invid", 761 realm: "chromium:ci", 762 createTime: time.Unix(1000, 0), 763 } 764 reqs := []*artifactCreationRequest{ 765 { 766 testID: "testid", 767 resultID: "resultid", 768 artifactID: "artifactid", 769 contentType: "text/plain", 770 size: 20, 771 data: []byte("0123456789abcdefghij"), 772 }, 773 } 774 err := uploadArtifactsToBQ(ctx, client, reqs, invInfo) 775 So(err, ShouldNotBeNil) 776 So(artifactExportCounter.Get(ctx, "chromium", "failure_input"), ShouldEqual, 1) 777 }) 778 }