go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/sink/sink_server_test.go (about) 1 // Copyright 2020 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 sink 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 "testing" 22 "time" 23 24 . "github.com/smartystreets/goconvey/convey" 25 "google.golang.org/grpc/codes" 26 "google.golang.org/grpc/metadata" 27 "google.golang.org/protobuf/proto" 28 "google.golang.org/protobuf/types/known/durationpb" 29 "google.golang.org/protobuf/types/known/structpb" 30 31 . "go.chromium.org/luci/common/testing/assertions" 32 "go.chromium.org/luci/resultdb/pbutil" 33 pb "go.chromium.org/luci/resultdb/proto/v1" 34 sinkpb "go.chromium.org/luci/resultdb/sink/proto/v1" 35 ) 36 37 func TestReportTestResults(t *testing.T) { 38 t.Parallel() 39 40 ctx := metadata.NewIncomingContext( 41 context.Background(), 42 metadata.Pairs(AuthTokenKey, authTokenValue("secret"))) 43 44 Convey("ReportTestResults", t, func() { 45 // close and drain the server to enforce all the requests processed. 46 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 47 defer cancel() 48 49 cfg := testServerConfig("", "secret") 50 tr, cleanup := validTestResult() 51 defer cleanup() 52 53 var sentTRReq *pb.BatchCreateTestResultsRequest 54 cfg.Recorder.(*mockRecorder).batchCreateTestResults = func(c context.Context, in *pb.BatchCreateTestResultsRequest) (*pb.BatchCreateTestResultsResponse, error) { 55 sentTRReq = in 56 return nil, nil 57 } 58 var sentArtReq *pb.BatchCreateArtifactsRequest 59 cfg.Recorder.(*mockRecorder).batchCreateArtifacts = func(ctx context.Context, in *pb.BatchCreateArtifactsRequest) (*pb.BatchCreateArtifactsResponse, error) { 60 sentArtReq = in 61 return nil, nil 62 } 63 var sentExoReq *pb.BatchCreateTestExonerationsRequest 64 cfg.Recorder.(*mockRecorder).batchCreateTestExonerations = func(ctx context.Context, in *pb.BatchCreateTestExonerationsRequest) (*pb.BatchCreateTestExonerationsResponse, error) { 65 sentExoReq = in 66 return nil, nil 67 } 68 69 expectedTR := &pb.TestResult{ 70 TestId: tr.TestId, 71 ResultId: tr.ResultId, 72 Expected: tr.Expected, 73 Status: tr.Status, 74 SummaryHtml: tr.SummaryHtml, 75 StartTime: tr.StartTime, 76 Duration: tr.Duration, 77 Tags: tr.Tags, 78 Variant: tr.Variant, 79 TestMetadata: tr.TestMetadata, 80 FailureReason: tr.FailureReason, 81 } 82 83 checkResults := func() { 84 sink, err := newSinkServer(ctx, cfg) 85 sink.(*sinkpb.DecoratedSink).Service.(*sinkServer).resultIDBase = "foo" 86 sink.(*sinkpb.DecoratedSink).Service.(*sinkServer).resultCounter = 100 87 So(err, ShouldBeNil) 88 defer closeSinkServer(ctx, sink) 89 90 req := &sinkpb.ReportTestResultsRequest{ 91 TestResults: []*sinkpb.TestResult{tr}, 92 } 93 // Clone because the RPC impl mutates the request objects. 94 req = proto.Clone(req).(*sinkpb.ReportTestResultsRequest) 95 _, err = sink.ReportTestResults(ctx, req) 96 So(err, ShouldBeNil) 97 98 closeSinkServer(ctx, sink) 99 So(sentTRReq, ShouldNotBeNil) 100 So(sentTRReq.Requests, ShouldHaveLength, 1) 101 So(sentTRReq.Requests[0].TestResult, ShouldResembleProto, expectedTR) 102 } 103 104 Convey("works", func() { 105 Convey("with ServerConfig.TestIDPrefix", func() { 106 cfg.TestIDPrefix = "ninja://foo/bar/" 107 tr.TestId = "HelloWorld.TestA" 108 expectedTR.TestId = "ninja://foo/bar/HelloWorld.TestA" 109 checkResults() 110 }) 111 112 Convey("with ServerConfig.BaseVariant", func() { 113 base := []string{"bucket", "try", "builder", "linux-rel"} 114 cfg.BaseVariant = pbutil.Variant(base...) 115 expectedTR.Variant = pbutil.Variant(base...) 116 checkResults() 117 }) 118 119 Convey("with ServerConfig.BaseTags", func() { 120 t1, t2 := pbutil.StringPairs("t1", "v1"), pbutil.StringPairs("t2", "v2") 121 // (nil, nil) 122 cfg.BaseTags, tr.Tags, expectedTR.Tags = nil, nil, nil 123 checkResults() 124 125 // (tag, nil) 126 cfg.BaseTags, tr.Tags, expectedTR.Tags = t1, nil, t1 127 checkResults() 128 129 // (nil, tag) 130 cfg.BaseTags, tr.Tags, expectedTR.Tags = nil, t1, t1 131 checkResults() 132 133 // (tag1, tag2) 134 cfg.BaseTags, tr.Tags, expectedTR.Tags = t1, t2, append(t1, t2...) 135 checkResults() 136 }) 137 138 Convey("with ServerConfig.BaseVariant and test result variant", func() { 139 v1, v2 := pbutil.Variant("bucket", "try"), pbutil.Variant("builder", "linux-rel") 140 // (nil, nil) 141 cfg.BaseVariant, tr.Variant, expectedTR.Variant = nil, nil, nil 142 checkResults() 143 144 // (variant, nil) 145 cfg.BaseVariant, tr.Variant, expectedTR.Variant = v1, nil, v1 146 checkResults() 147 148 // (nil, variant) 149 cfg.BaseVariant, tr.Variant, expectedTR.Variant = nil, v1, v1 150 checkResults() 151 152 // (variant1, variant2) 153 cfg.BaseVariant, tr.Variant, expectedTR.Variant = v1, v2, pbutil.CombineVariant(v1, v2) 154 checkResults() 155 }) 156 }) 157 158 Convey("generates a random ResultID, if omitted", func() { 159 tr.ResultId = "" 160 expectedTR.ResultId = "foo-00101" 161 checkResults() 162 }) 163 164 Convey("duration", func() { 165 Convey("with CoerceNegativeDuration", func() { 166 cfg.CoerceNegativeDuration = true 167 168 // duration == nil 169 tr.Duration, expectedTR.Duration = nil, nil 170 checkResults() 171 172 // duration == 0 173 tr.Duration, expectedTR.Duration = durationpb.New(0), durationpb.New(0) 174 checkResults() 175 176 // duration > 0 177 tr.Duration, expectedTR.Duration = durationpb.New(8), durationpb.New(8) 178 checkResults() 179 180 // duration < 0 181 tr.Duration = durationpb.New(-8) 182 expectedTR.Duration = durationpb.New(0) 183 checkResults() 184 }) 185 Convey("without CoerceNegativeDuration", func() { 186 // duration < 0 187 tr.Duration = durationpb.New(-8) 188 sink, err := newSinkServer(ctx, cfg) 189 So(err, ShouldBeNil) 190 191 req := &sinkpb.ReportTestResultsRequest{TestResults: []*sinkpb.TestResult{tr}} 192 _, err = sink.ReportTestResults(ctx, req) 193 So(err, ShouldErrLike, "duration: is < 0") 194 }) 195 }) 196 197 Convey("failure reason", func() { 198 Convey("specified", func() { 199 tr.FailureReason = &pb.FailureReason{ 200 PrimaryErrorMessage: "Example failure reason.", 201 Errors: []*pb.FailureReason_Error{ 202 {Message: "Example failure reason."}, 203 {Message: "Example failure reason2."}, 204 }, 205 TruncatedErrorsCount: 0, 206 } 207 expectedTR.FailureReason = &pb.FailureReason{ 208 PrimaryErrorMessage: "Example failure reason.", 209 Errors: []*pb.FailureReason_Error{ 210 {Message: "Example failure reason."}, 211 {Message: "Example failure reason2."}, 212 }, 213 TruncatedErrorsCount: 0, 214 } 215 checkResults() 216 }) 217 218 Convey("nil", func() { 219 tr.FailureReason = nil 220 expectedTR.FailureReason = nil 221 checkResults() 222 }) 223 224 Convey("primary_error_message too long", func() { 225 var b strings.Builder 226 // Make a string that exceeds the 1024-byte length limit 227 // (when encoded as UTF-8). 228 for i := 0; i < 1025; i++ { 229 b.WriteRune('.') 230 } 231 tr.FailureReason = &pb.FailureReason{ 232 PrimaryErrorMessage: b.String(), 233 } 234 235 sink, err := newSinkServer(ctx, cfg) 236 So(err, ShouldBeNil) 237 238 req := &sinkpb.ReportTestResultsRequest{TestResults: []*sinkpb.TestResult{tr}} 239 _, err = sink.ReportTestResults(ctx, req) 240 So(err, ShouldErrLike, 241 "failure_reason: primary_error_message: exceeds the"+ 242 " maximum size of 1024 bytes") 243 }) 244 245 Convey("error_messages too long", func() { 246 var b strings.Builder 247 // Make a string that exceeds the 1024-byte length limit 248 // (when encoded as UTF-8). 249 for i := 0; i < 1025; i++ { 250 b.WriteRune('.') 251 } 252 tr.FailureReason = &pb.FailureReason{ 253 PrimaryErrorMessage: "Example failure reason.", 254 Errors: []*pb.FailureReason_Error{ 255 {Message: "Example failure reason."}, 256 {Message: b.String()}, 257 }, 258 TruncatedErrorsCount: 0, 259 } 260 261 sink, err := newSinkServer(ctx, cfg) 262 So(err, ShouldBeNil) 263 264 req := &sinkpb.ReportTestResultsRequest{ 265 TestResults: []*sinkpb.TestResult{tr}, 266 } 267 _, err = sink.ReportTestResults(ctx, req) 268 So(err, ShouldErrLike, 269 fmt.Sprintf("errors[1]: message: exceeds the maximum "+ 270 "size of 1024 bytes")) 271 }) 272 }) 273 274 Convey("properties", func() { 275 Convey("specified", func() { 276 tr.Properties = &structpb.Struct{ 277 Fields: map[string]*structpb.Value{ 278 "key_1": structpb.NewStringValue("value_1"), 279 "key_2": structpb.NewStructValue(&structpb.Struct{ 280 Fields: map[string]*structpb.Value{ 281 "child_key": structpb.NewNumberValue(1), 282 }, 283 }), 284 }, 285 } 286 expectedTR.Properties = &structpb.Struct{ 287 Fields: map[string]*structpb.Value{ 288 "key_1": structpb.NewStringValue("value_1"), 289 "key_2": structpb.NewStructValue(&structpb.Struct{ 290 Fields: map[string]*structpb.Value{ 291 "child_key": structpb.NewNumberValue(1), 292 }, 293 }), 294 }, 295 } 296 checkResults() 297 }) 298 299 Convey("nil", func() { 300 tr.Properties = nil 301 expectedTR.Properties = nil 302 checkResults() 303 }) 304 305 Convey("properties too large", func() { 306 tr.Properties = &structpb.Struct{ 307 Fields: map[string]*structpb.Value{ 308 "key1": structpb.NewStringValue(strings.Repeat("1", pbutil.MaxSizeProperties)), 309 }, 310 } 311 312 sink, err := newSinkServer(ctx, cfg) 313 So(err, ShouldBeNil) 314 315 req := &sinkpb.ReportTestResultsRequest{TestResults: []*sinkpb.TestResult{tr}} 316 _, err = sink.ReportTestResults(ctx, req) 317 So(err, ShouldErrLike, `properties: exceeds the maximum size of`, `bytes`) 318 }) 319 }) 320 321 Convey("with ServerConfig.TestLocationBase", func() { 322 cfg.TestLocationBase = "//base/" 323 tr.TestMetadata.Location.FileName = "artifact_dir/a_test.cc" 324 expectedTR.TestMetadata = proto.Clone(expectedTR.TestMetadata).(*pb.TestMetadata) 325 expectedTR.TestMetadata.Location.FileName = "//base/artifact_dir/a_test.cc" 326 checkResults() 327 }) 328 329 subTags := pbutil.StringPairs( 330 "feature", "feature2", 331 "feature", "feature3", 332 "monorail_component", "Monorail>Component>Sub", 333 ) 334 subComponent := &pb.BugComponent{ 335 System: &pb.BugComponent_IssueTracker{ 336 IssueTracker: &pb.IssueTrackerComponent{ 337 ComponentId: 222, 338 }, 339 }, 340 } 341 342 rootTags := pbutil.StringPairs( 343 "feature", "feature1", 344 "monorail_component", "Monorail>Component", 345 "teamEmail", "team_email@chromium.org", 346 "os", "WINDOWS", 347 ) 348 rootComponent := &pb.BugComponent{ 349 System: &pb.BugComponent_IssueTracker{ 350 IssueTracker: &pb.IssueTrackerComponent{ 351 ComponentId: 111, 352 }, 353 }, 354 } 355 356 Convey("with ServerConfig.LocationTags", func() { 357 cfg.LocationTags = &sinkpb.LocationTags{ 358 Repos: map[string]*sinkpb.LocationTags_Repo{ 359 "https://chromium.googlesource.com/chromium/src": { 360 Dirs: map[string]*sinkpb.LocationTags_Dir{ 361 ".": { 362 Tags: rootTags, 363 BugComponent: rootComponent, 364 }, 365 "artifact_dir": { 366 Tags: subTags, 367 BugComponent: subComponent, 368 }, 369 }, 370 }, 371 }, 372 } 373 expectedTR.Tags = append(expectedTR.Tags, pbutil.StringPairs( 374 "feature", "feature2", 375 "feature", "feature3", 376 "monorail_component", "Monorail>Component>Sub", 377 "teamEmail", "team_email@chromium.org", 378 "os", "WINDOWS", 379 )...) 380 expectedTR.TestMetadata.BugComponent = subComponent 381 pbutil.SortStringPairs(expectedTR.Tags) 382 checkResults() 383 }) 384 385 Convey("with ServerConfig.LocationTags file based", func() { 386 overriddenTags := pbutil.StringPairs( 387 "featureX", "featureY", 388 "monorail_component", "Monorail>File>Component", 389 ) 390 overriddenComponent := &pb.BugComponent{ 391 System: &pb.BugComponent_IssueTracker{ 392 IssueTracker: &pb.IssueTrackerComponent{ 393 ComponentId: 333, 394 }, 395 }, 396 } 397 398 cfg.LocationTags = &sinkpb.LocationTags{ 399 Repos: map[string]*sinkpb.LocationTags_Repo{ 400 "https://chromium.googlesource.com/chromium/src": { 401 Files: map[string]*sinkpb.LocationTags_File{ 402 "artifact_dir/a_test.cc": { 403 Tags: overriddenTags, 404 BugComponent: overriddenComponent, 405 }, 406 }, 407 Dirs: map[string]*sinkpb.LocationTags_Dir{ 408 ".": { 409 Tags: rootTags, 410 BugComponent: rootComponent, 411 }, 412 "artifact_dir": { 413 Tags: subTags, 414 BugComponent: subComponent, 415 }, 416 }, 417 }, 418 }, 419 } 420 421 expectedTR.Tags = append(expectedTR.Tags, pbutil.StringPairs( 422 "feature", "feature2", 423 "feature", "feature3", 424 "featureX", "featureY", 425 "monorail_component", "Monorail>File>Component", 426 "teamEmail", "team_email@chromium.org", 427 "os", "WINDOWS", 428 )...) 429 expectedTR.TestMetadata.BugComponent = overriddenComponent 430 pbutil.SortStringPairs(expectedTR.Tags) 431 432 checkResults() 433 }) 434 435 Convey("ReportTestResults", func() { 436 sink, err := newSinkServer(ctx, cfg) 437 So(err, ShouldBeNil) 438 defer closeSinkServer(ctx, sink) 439 440 report := func(trs ...*sinkpb.TestResult) error { 441 _, err := sink.ReportTestResults(ctx, &sinkpb.ReportTestResultsRequest{TestResults: trs}) 442 return err 443 } 444 445 Convey("returns an error if the artifact req is invalid", func() { 446 tr.Artifacts["art2"] = &sinkpb.Artifact{} 447 So(report(tr), ShouldHaveRPCCode, codes.InvalidArgument, 448 "one of file_path or contents or gcs_uri must be provided") 449 }) 450 451 Convey("with an inaccesible artifact file", func() { 452 tr.Artifacts["art2"] = &sinkpb.Artifact{ 453 Body: &sinkpb.Artifact_FilePath{FilePath: "not_exist"}} 454 455 Convey("drops the artifact", func() { 456 So(report(tr), ShouldBeRPCOK) 457 458 // make sure that no TestResults were dropped, and the valid artifact, "art1", 459 // was not dropped, either. 460 closeSinkServer(ctx, sink) 461 So(sentTRReq, ShouldNotBeNil) 462 So(sentTRReq.Requests, ShouldHaveLength, 1) 463 So(sentTRReq.Requests[0].TestResult, ShouldResembleProto, expectedTR) 464 465 So(sentArtReq, ShouldNotBeNil) 466 So(sentArtReq.Requests, ShouldHaveLength, 1) 467 So(sentArtReq.Requests[0].Artifact, ShouldResembleProto, &pb.Artifact{ 468 ArtifactId: "art1", 469 ContentType: "text/plain", 470 Contents: []byte("a sample artifact"), 471 SizeBytes: int64(len("a sample artifact")), 472 }) 473 }) 474 }) 475 }) 476 477 Convey("report exoneration", func() { 478 cfg.ExonerateUnexpectedPass = true 479 sink, err := newSinkServer(ctx, cfg) 480 So(err, ShouldBeNil) 481 defer closeSinkServer(ctx, sink) 482 483 Convey("exonerate unexpected pass", func() { 484 tr.Expected = false 485 486 _, err = sink.ReportTestResults(ctx, &sinkpb.ReportTestResultsRequest{TestResults: []*sinkpb.TestResult{tr}}) 487 So(err, ShouldBeRPCOK) 488 closeSinkServer(ctx, sink) 489 So(sentExoReq, ShouldNotBeNil) 490 So(sentExoReq.Requests, ShouldHaveLength, 1) 491 So(sentExoReq.Requests[0].TestExoneration, ShouldResembleProto, &pb.TestExoneration{ 492 TestId: tr.TestId, 493 ExplanationHtml: "Unexpected passes are exonerated", 494 Reason: pb.ExonerationReason_UNEXPECTED_PASS, 495 }) 496 }) 497 498 Convey("not exonerate unexpected failure", func() { 499 tr.Expected = false 500 tr.Status = pb.TestStatus_FAIL 501 502 _, err = sink.ReportTestResults(ctx, &sinkpb.ReportTestResultsRequest{TestResults: []*sinkpb.TestResult{tr}}) 503 So(err, ShouldBeRPCOK) 504 closeSinkServer(ctx, sink) 505 So(sentExoReq, ShouldBeNil) 506 }) 507 508 Convey("not exonerate expected pass", func() { 509 _, err = sink.ReportTestResults(ctx, &sinkpb.ReportTestResultsRequest{TestResults: []*sinkpb.TestResult{tr}}) 510 So(err, ShouldBeRPCOK) 511 closeSinkServer(ctx, sink) 512 So(sentExoReq, ShouldBeNil) 513 }) 514 515 Convey("not exonerate expected failure", func() { 516 tr.Status = pb.TestStatus_FAIL 517 518 _, err = sink.ReportTestResults(ctx, &sinkpb.ReportTestResultsRequest{TestResults: []*sinkpb.TestResult{tr}}) 519 So(err, ShouldBeRPCOK) 520 closeSinkServer(ctx, sink) 521 So(sentExoReq, ShouldBeNil) 522 }) 523 }) 524 }) 525 } 526 527 func TestReportInvocationLevelArtifacts(t *testing.T) { 528 t.Parallel() 529 530 Convey("ReportInvocationLevelArtifacts", t, func() { 531 ctx := metadata.NewIncomingContext( 532 context.Background(), 533 metadata.Pairs(AuthTokenKey, authTokenValue("secret"))) 534 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 535 defer cancel() 536 537 cfg := testServerConfig("", "secret") 538 sink, err := newSinkServer(ctx, cfg) 539 So(err, ShouldBeNil) 540 defer closeSinkServer(ctx, sink) 541 542 art1 := &sinkpb.Artifact{Body: &sinkpb.Artifact_Contents{Contents: []byte("123")}} 543 art2 := &sinkpb.Artifact{Body: &sinkpb.Artifact_GcsUri{GcsUri: "gs://bucket/foo"}} 544 545 req := &sinkpb.ReportInvocationLevelArtifactsRequest{ 546 Artifacts: map[string]*sinkpb.Artifact{"art1": art1, "art2": art2}, 547 } 548 _, err = sink.ReportInvocationLevelArtifacts(ctx, req) 549 So(err, ShouldBeNil) 550 551 // Duplicated artifact will be rejected. 552 _, err = sink.ReportInvocationLevelArtifacts(ctx, req) 553 So(err, ShouldErrLike, ` has already been uploaded`) 554 }) 555 }