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 }