go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/create_test_result_test.go (about)

     1  // Copyright 2019 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  	"fmt"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/golang/protobuf/proto"
    23  	. "github.com/smartystreets/goconvey/convey"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/metadata"
    26  	"google.golang.org/protobuf/types/known/structpb"
    27  
    28  	"go.chromium.org/luci/common/clock"
    29  	"go.chromium.org/luci/common/clock/testclock"
    30  	. "go.chromium.org/luci/common/testing/assertions"
    31  	"go.chromium.org/luci/resultdb/internal/invocations"
    32  	"go.chromium.org/luci/resultdb/internal/resultcount"
    33  	"go.chromium.org/luci/resultdb/internal/testresults"
    34  	"go.chromium.org/luci/resultdb/internal/testutil"
    35  	"go.chromium.org/luci/resultdb/internal/testutil/insert"
    36  	"go.chromium.org/luci/resultdb/pbutil"
    37  	pb "go.chromium.org/luci/resultdb/proto/v1"
    38  	"go.chromium.org/luci/server/span"
    39  )
    40  
    41  // validCreateTestResultRequest returns a valid CreateTestResultRequest message.
    42  func validCreateTestResultRequest(now time.Time, invName, testID string) *pb.CreateTestResultRequest {
    43  	trName := fmt.Sprintf("invocations/%s/tests/%s/results/result-id-0", invName, testID)
    44  	properties, err := structpb.NewStruct(map[string]any{
    45  		"key_1": "value_1",
    46  		"key_2": map[string]any{
    47  			"child_key": 1,
    48  		},
    49  	})
    50  	if err != nil {
    51  		// Should never fail.
    52  		panic(err)
    53  	}
    54  
    55  	return &pb.CreateTestResultRequest{
    56  		Invocation: invName,
    57  		RequestId:  "request-id-123",
    58  
    59  		TestResult: &pb.TestResult{
    60  			Name:     trName,
    61  			TestId:   testID,
    62  			ResultId: "result-id-0",
    63  			Expected: true,
    64  			Status:   pb.TestStatus_PASS,
    65  			Variant: pbutil.Variant(
    66  				"a/b", "1",
    67  				"c", "2",
    68  			),
    69  			TestMetadata: &pb.TestMetadata{
    70  				Name: "original_name",
    71  				Location: &pb.TestLocation{
    72  					Repo:     "https://chromium.googlesource.com/chromium/src",
    73  					FileName: "//a_test.go",
    74  					Line:     54,
    75  				},
    76  				BugComponent: &pb.BugComponent{
    77  					System: &pb.BugComponent_Monorail{
    78  						Monorail: &pb.MonorailComponent{
    79  							Project: "chromium",
    80  							Value:   "Component>Value",
    81  						},
    82  					},
    83  				},
    84  			},
    85  			FailureReason: &pb.FailureReason{
    86  				PrimaryErrorMessage: "got 1, want 0",
    87  				Errors: []*pb.FailureReason_Error{
    88  					{Message: "got 1, want 0"},
    89  					{Message: "got 2, want 1"},
    90  				},
    91  				TruncatedErrorsCount: 0,
    92  			},
    93  			Properties: properties,
    94  		},
    95  	}
    96  }
    97  
    98  func TestValidateCreateTestResultRequest(t *testing.T) {
    99  	t.Parallel()
   100  
   101  	now := testclock.TestRecentTimeUTC
   102  	Convey("ValidateCreateTestResultRequest", t, func() {
   103  		req := validCreateTestResultRequest(now, "invocations/u-build-1", "test-id")
   104  
   105  		Convey("suceeeds", func() {
   106  			So(validateCreateTestResultRequest(req, now), ShouldBeNil)
   107  
   108  			Convey("with empty request_id", func() {
   109  				req.RequestId = ""
   110  				So(validateCreateTestResultRequest(req, now), ShouldBeNil)
   111  			})
   112  		})
   113  
   114  		Convey("fails with ", func() {
   115  			Convey(`empty invocation`, func() {
   116  				req.Invocation = ""
   117  				err := validateCreateTestResultRequest(req, now)
   118  				So(err, ShouldErrLike, "invocation: unspecified")
   119  			})
   120  			Convey(`invalid invocation`, func() {
   121  				req.Invocation = " invalid "
   122  				err := validateCreateTestResultRequest(req, now)
   123  				So(err, ShouldErrLike, "invocation: does not match")
   124  			})
   125  
   126  			Convey(`empty test_result`, func() {
   127  				req.TestResult = nil
   128  				err := validateCreateTestResultRequest(req, now)
   129  				So(err, ShouldErrLike, "test_result: unspecified")
   130  			})
   131  			Convey(`invalid test_result`, func() {
   132  				req.TestResult.TestId = ""
   133  				err := validateCreateTestResultRequest(req, now)
   134  				So(err, ShouldErrLike, "test_result: test_id: unspecified")
   135  			})
   136  
   137  			Convey("invalid request_id", func() {
   138  				// non-ascii character
   139  				req.RequestId = string(rune(244))
   140  				err := validateCreateTestResultRequest(req, now)
   141  				So(err, ShouldErrLike, "request_id: does not match")
   142  			})
   143  		})
   144  	})
   145  }
   146  
   147  func TestCreateTestResult(t *testing.T) {
   148  	Convey(`CreateTestResult`, t, func() {
   149  		ctx := testutil.SpannerTestContext(t)
   150  		recorder := newTestRecorderServer()
   151  		req := validCreateTestResultRequest(
   152  			clock.Now(ctx).UTC(), "invocations/u-build-1", "test-id",
   153  		)
   154  
   155  		createTestResult := func(req *pb.CreateTestResultRequest, expectedCommonPrefix string) {
   156  			expected := proto.Clone(req.TestResult).(*pb.TestResult)
   157  			expected.Name = "invocations/u-build-1/tests/test-id/results/result-id-0"
   158  			expected.VariantHash = "c8643f74854d84b4"
   159  			res, err := recorder.CreateTestResult(ctx, req)
   160  			So(err, ShouldBeNil)
   161  			So(res, ShouldResembleProto, expected)
   162  
   163  			// double-check it with the database
   164  			expected.VariantHash = "c8643f74854d84b4"
   165  			row, err := testresults.Read(span.Single(ctx), res.Name)
   166  			So(err, ShouldBeNil)
   167  			So(row, ShouldResembleProto, expected)
   168  
   169  			var invCommonTestIdPrefix string
   170  			err = invocations.ReadColumns(span.Single(ctx), invocations.ID("u-build-1"), map[string]any{"CommonTestIDPrefix": &invCommonTestIdPrefix})
   171  			So(err, ShouldBeNil)
   172  			So(invCommonTestIdPrefix, ShouldEqual, expectedCommonPrefix)
   173  		}
   174  
   175  		// Insert a sample invocation
   176  		tok, err := generateInvocationToken(ctx, "u-build-1")
   177  		So(err, ShouldBeNil)
   178  		ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(pb.UpdateTokenMetadataKey, tok))
   179  		invID := invocations.ID("u-build-1")
   180  		testutil.MustApply(ctx, insert.Invocation(invID, pb.Invocation_ACTIVE, nil))
   181  
   182  		Convey("succeeds", func() {
   183  			Convey("with a request ID", func() {
   184  				createTestResult(req, "test-id")
   185  
   186  				ctx, cancel := span.ReadOnlyTransaction(ctx)
   187  				defer cancel()
   188  				trNum, err := resultcount.ReadTestResultCount(ctx, invocations.NewIDSet("u-build-1"))
   189  				So(err, ShouldBeNil)
   190  				So(trNum, ShouldEqual, 1)
   191  			})
   192  
   193  			Convey("without a request ID", func() {
   194  				req.RequestId = ""
   195  				createTestResult(req, "test-id")
   196  			})
   197  		})
   198  
   199  		Convey("fails", func() {
   200  			Convey("with an invalid request", func() {
   201  				req.Invocation = "this is an invalid invocation name"
   202  				_, err := recorder.CreateTestResult(ctx, req)
   203  				So(err, ShouldHaveAppStatus, codes.InvalidArgument, "bad request: invocation: does not match")
   204  			})
   205  
   206  			Convey("with an non-existing invocation", func() {
   207  				tok, err = generateInvocationToken(ctx, "inv")
   208  				So(err, ShouldBeNil)
   209  				ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(pb.UpdateTokenMetadataKey, tok))
   210  				req.Invocation = "invocations/inv"
   211  				_, err := recorder.CreateTestResult(ctx, req)
   212  				So(err, ShouldHaveAppStatus, codes.NotFound, "invocations/inv not found")
   213  			})
   214  		})
   215  	})
   216  }