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  }