go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/create_test_exoneration_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  	"testing"
    19  
    20  	"github.com/golang/protobuf/proto"
    21  	. "github.com/smartystreets/goconvey/convey"
    22  	"google.golang.org/grpc/codes"
    23  	"google.golang.org/grpc/metadata"
    24  
    25  	. "go.chromium.org/luci/common/testing/assertions"
    26  	"go.chromium.org/luci/resultdb/internal/exonerations"
    27  	"go.chromium.org/luci/resultdb/internal/invocations"
    28  	"go.chromium.org/luci/resultdb/internal/testutil"
    29  	"go.chromium.org/luci/resultdb/internal/testutil/insert"
    30  	"go.chromium.org/luci/resultdb/pbutil"
    31  	pb "go.chromium.org/luci/resultdb/proto/v1"
    32  	"go.chromium.org/luci/server/span"
    33  )
    34  
    35  func TestValidateCreateTestExonerationRequest(t *testing.T) {
    36  	t.Parallel()
    37  	Convey(`TestValidateCreateTestExonerationRequest`, t, func() {
    38  		req := &pb.CreateTestExonerationRequest{
    39  			Invocation: "invocations/inv",
    40  			TestExoneration: &pb.TestExoneration{
    41  				TestId: "ninja://ab/cd.ef",
    42  				Variant: pbutil.Variant(
    43  					"a/b", "1",
    44  					"c", "2",
    45  				),
    46  				ExplanationHtml: "The test also failed without patch",
    47  				Reason:          pb.ExonerationReason_OCCURS_ON_MAINLINE,
    48  			},
    49  		}
    50  
    51  		Convey(`Empty Invocation`, func() {
    52  			req.Invocation = ""
    53  			err := validateCreateTestExonerationRequest(req, true)
    54  			So(err, ShouldErrLike, `invocation: unspecified`)
    55  		})
    56  
    57  		Convey(`Empty Exoneration`, func() {
    58  			req.TestExoneration = nil
    59  			err := validateCreateTestExonerationRequest(req, true)
    60  			So(err, ShouldErrLike, `test_exoneration: test_id: unspecified`)
    61  		})
    62  
    63  		Convey(`NUL in test id`, func() {
    64  			req.TestExoneration.TestId = "\x01"
    65  			err := validateCreateTestExonerationRequest(req, true)
    66  			So(err, ShouldErrLike, "test_id: non-printable rune")
    67  		})
    68  
    69  		Convey(`Invalid variant`, func() {
    70  			req.TestExoneration.Variant = pbutil.Variant("", "")
    71  			err := validateCreateTestExonerationRequest(req, true)
    72  			So(err, ShouldErrLike, `variant: "":"": key: unspecified`)
    73  		})
    74  
    75  		Convey(`Reason not specified`, func() {
    76  			req.TestExoneration.Reason = pb.ExonerationReason_EXONERATION_REASON_UNSPECIFIED
    77  			err := validateCreateTestExonerationRequest(req, true)
    78  			So(err, ShouldErrLike, `test_exoneration: reason: unspecified`)
    79  		})
    80  
    81  		Convey(`Explanation HTML not specified`, func() {
    82  			req.TestExoneration.ExplanationHtml = ""
    83  			err := validateCreateTestExonerationRequest(req, true)
    84  			So(err, ShouldErrLike, `test_exoneration: explanation_html: unspecified`)
    85  		})
    86  
    87  		Convey(`Valid`, func() {
    88  			err := validateCreateTestExonerationRequest(req, true)
    89  			So(err, ShouldBeNil)
    90  		})
    91  
    92  		Convey(`Mismatching variant hashes`, func() {
    93  			req.TestExoneration.VariantHash = "doesn't match"
    94  			err := validateCreateTestExonerationRequest(req, true)
    95  			So(err, ShouldErrLike, `computed and supplied variant hash don't match`)
    96  		})
    97  
    98  		Convey(`Matching variant hashes`, func() {
    99  			req.TestExoneration.Variant = pbutil.Variant("a", "b")
   100  			req.TestExoneration.VariantHash = "c467ccce5a16dc72"
   101  			err := validateCreateTestExonerationRequest(req, true)
   102  			So(err, ShouldBeNil)
   103  		})
   104  	})
   105  }
   106  
   107  func TestCreateTestExoneration(t *testing.T) {
   108  	Convey(`TestCreateTestExoneration`, t, func() {
   109  		ctx := testutil.SpannerTestContext(t)
   110  
   111  		recorder := newTestRecorderServer()
   112  
   113  		token, err := generateInvocationToken(ctx, "inv")
   114  		So(err, ShouldBeNil)
   115  		ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(pb.UpdateTokenMetadataKey, token))
   116  
   117  		Convey(`Invalid request`, func() {
   118  			req := &pb.CreateTestExonerationRequest{
   119  				Invocation: "invocations/inv",
   120  				TestExoneration: &pb.TestExoneration{
   121  					TestId:          "\x01",
   122  					ExplanationHtml: "Unexpected pass.",
   123  					Reason:          pb.ExonerationReason_UNEXPECTED_PASS,
   124  				},
   125  			}
   126  			_, err := recorder.CreateTestExoneration(ctx, req)
   127  			So(err, ShouldHaveAppStatus, codes.InvalidArgument, `bad request: test_exoneration: test_id: non-printable rune`)
   128  		})
   129  
   130  		Convey(`No invocation`, func() {
   131  			req := &pb.CreateTestExonerationRequest{
   132  				Invocation: "invocations/inv",
   133  				TestExoneration: &pb.TestExoneration{
   134  					TestId:          "a",
   135  					ExplanationHtml: "Unexpected pass.",
   136  					Reason:          pb.ExonerationReason_UNEXPECTED_PASS,
   137  				},
   138  			}
   139  			_, err := recorder.CreateTestExoneration(ctx, req)
   140  			So(err, ShouldHaveAppStatus, codes.NotFound, `invocations/inv not found`)
   141  		})
   142  
   143  		// Insert the invocation.
   144  		testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil))
   145  
   146  		e2eTest := func(req *pb.CreateTestExonerationRequest, expectedVariantHash, expectedId string) {
   147  			res, err := recorder.CreateTestExoneration(ctx, req)
   148  			So(err, ShouldBeNil)
   149  
   150  			if expectedId == "" {
   151  				So(res.ExonerationId, ShouldStartWith, expectedVariantHash+":")
   152  			} else {
   153  				So(res.ExonerationId, ShouldEqual, expectedVariantHash+":"+expectedId)
   154  			}
   155  
   156  			expected := proto.Clone(req.TestExoneration).(*pb.TestExoneration)
   157  			proto.Merge(expected, &pb.TestExoneration{
   158  				Name:          pbutil.TestExonerationName("inv", "a", res.ExonerationId),
   159  				ExonerationId: res.ExonerationId,
   160  				VariantHash:   expectedVariantHash,
   161  			})
   162  			So(res, ShouldResembleProto, expected)
   163  
   164  			// Now check the database.
   165  			row, err := exonerations.Read(span.Single(ctx), res.Name)
   166  			So(err, ShouldBeNil)
   167  			So(row.Variant, ShouldResembleProto, expected.Variant)
   168  			So(row.ExplanationHtml, ShouldEqual, expected.ExplanationHtml)
   169  
   170  			// Check variant hash.
   171  			key := invocations.ID("inv").Key(res.TestId, res.ExonerationId)
   172  			var variantHash string
   173  			testutil.MustReadRow(ctx, "TestExonerations", key, map[string]any{
   174  				"VariantHash": &variantHash,
   175  			})
   176  			So(variantHash, ShouldEqual, expectedVariantHash)
   177  
   178  			if req.RequestId != "" {
   179  				// Test idempotency.
   180  				res2, err := recorder.CreateTestExoneration(ctx, req)
   181  				So(err, ShouldBeNil)
   182  				So(res2, ShouldResembleProto, res)
   183  			}
   184  		}
   185  
   186  		Convey(`Without request id, e2e`, func() {
   187  			e2eTest(&pb.CreateTestExonerationRequest{
   188  				Invocation: "invocations/inv",
   189  				TestExoneration: &pb.TestExoneration{
   190  					TestId:          "a",
   191  					Variant:         pbutil.Variant("a", "1", "b", "2"),
   192  					ExplanationHtml: "Test is known flaky. Similar test failures have been observed in other CLs.",
   193  					Reason:          pb.ExonerationReason_OCCURS_ON_OTHER_CLS,
   194  				},
   195  			}, "6408fdc5c36df5df", "")
   196  		})
   197  
   198  		Convey(`With request id, e2e`, func() {
   199  			e2eTest(&pb.CreateTestExonerationRequest{
   200  				RequestId:  "request id",
   201  				Invocation: "invocations/inv",
   202  				TestExoneration: &pb.TestExoneration{
   203  					TestId:          "a",
   204  					Variant:         pbutil.Variant("a", "1", "b", "2"),
   205  					ExplanationHtml: "Test also failed when tried without patch.",
   206  					Reason:          pb.ExonerationReason_OCCURS_ON_MAINLINE,
   207  				},
   208  			}, "6408fdc5c36df5df", "d:2960f0231ce23039cdf7d4a62e31939ecd897bbf465e0fb2d35bf425ae1c5ae14eb0714d6dd0a0c244eaa66ae2b645b0637f58e91ed1b820bb1f01d8d4a72e67")
   209  		})
   210  
   211  		Convey(`With hash but no variant, e2e`, func() {
   212  			e2eTest(&pb.CreateTestExonerationRequest{
   213  				RequestId:  "request id",
   214  				Invocation: "invocations/inv",
   215  				TestExoneration: &pb.TestExoneration{
   216  					TestId:          "a",
   217  					VariantHash:     "deadbeefdeadbeef",
   218  					ExplanationHtml: "Unexpected pass.",
   219  					Reason:          pb.ExonerationReason_UNEXPECTED_PASS,
   220  				},
   221  			}, "deadbeefdeadbeef", "")
   222  		})
   223  	})
   224  }